From 47082dd19f3350169518810442ad7ff1b841600c Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer Date: Wed, 26 Aug 2026 09:04:01 +0200 Subject: [PATCH 1/2] Refactor code structure for improved readability and maintainability --- .github/scripts/pages-report/aic-usage.mjs | 130 ++ .../pages-report}/deployed-workflows.mjs | 221 ++- .../pages-report}/inventory.mjs | 13 +- .../pages-report}/report.mjs | 1227 ++++++++++++----- .github/skills/github-pages-report/SKILL.md | 156 --- .../dependabot-release-train-updater.md | 18 +- .github/workflows/dependabot.md | 22 +- .../optimization-ai-credit-auditor.md | 16 +- .../optimization-ai-credit-optimizer.md | 13 +- .github/workflows/optimization.md | 22 +- .../workflows/shared/control-precompute.md | 55 + .github/workflows/shared/control.md | 39 +- .github/workflows/shared/review-bundle.md | 19 +- .../shared/target-checkout-read-org-token.md | 1 + docs/operations.md | 18 +- pages/README.md | 17 +- pages/aw.yml | 8 - pages/pages.yml | 63 +- tests/integration/package-lifecycle.test.mjs | 4 - tests/load/control-plane-load.test.mjs | 1 + tests/unit/workflow-contract.test.mjs | 49 +- 21 files changed, 1457 insertions(+), 655 deletions(-) create mode 100644 .github/scripts/pages-report/aic-usage.mjs rename .github/{skills/github-pages-report => scripts/pages-report}/deployed-workflows.mjs (54%) rename .github/{skills/github-pages-report => scripts/pages-report}/inventory.mjs (90%) rename .github/{skills/github-pages-report => scripts/pages-report}/report.mjs (54%) delete mode 100644 .github/skills/github-pages-report/SKILL.md delete mode 100644 pages/aw.yml diff --git a/.github/scripts/pages-report/aic-usage.mjs b/.github/scripts/pages-report/aic-usage.mjs new file mode 100644 index 0000000..ea4d556 --- /dev/null +++ b/.github/scripts/pages-report/aic-usage.mjs @@ -0,0 +1,130 @@ +import { spawn } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +function runGhAw(repository, runIds, outputDirectory) { + return new Promise((resolve, reject) => { + const child = spawn("gh", [ + "aw", "logs", "--repo", repository, "--stdin", "--json", + "--output", outputDirectory, "--summary-file", "", "--cache-before", "-2d", + ], { env: process.env, stdio: ["pipe", "pipe", "pipe"] }); + const stdout = []; + const stderr = []; + let outputBytes = 0; + child.stdout.on("data", (chunk) => { + outputBytes += chunk.length; + if (outputBytes > 50 * 1024 * 1024) child.kill(); + else stdout.push(chunk); + }); + child.stderr.on("data", (chunk) => stderr.push(chunk)); + child.on("error", reject); + child.on("close", (code, signal) => { + const diagnostic = Buffer.concat(stderr).toString("utf8").trim(); + if (code === 0 && !signal) resolve(Buffer.concat(stdout).toString("utf8")); + else reject(new Error(diagnostic || `gh aw logs exited with ${signal || code}`)); + }); + child.stdin.end(`${[...runIds].join("\n")}\n`); + }); +} + +async function mapWithConcurrency(values, concurrency, mapper) { + const results = new Array(values.length); + let nextIndex = 0; + async function worker() { + while (nextIndex < values.length) { + const index = nextIndex; + nextIndex += 1; + results[index] = await mapper(values[index]); + } + } + await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, worker)); + return results; +} + +(async () => { + const inventoryPath = process.env.REPORT_DEPLOYED_WORKFLOWS; + const outputPath = path.resolve(process.env.REPORT_AIC_USAGE || "_inventory/aic-usage.json"); + const configuredCacheRoot = process.env.REPORT_AIC_CACHE ? path.resolve(process.env.REPORT_AIC_CACHE) : ""; + const requestedConcurrency = Number(process.env.REPORT_AIC_CONCURRENCY || 3); + const concurrency = Number.isInteger(requestedConcurrency) && requestedConcurrency > 0 + ? Math.min(requestedConcurrency, 8) + : 3; + if (!inventoryPath) throw new Error("REPORT_DEPLOYED_WORKFLOWS is required"); + + const inventory = JSON.parse(await readFile(inventoryPath, "utf8")); + const runIdsByRepository = new Map(); + const workflowByRun = new Map(); + for (const workflow of inventory.workflows || []) { + const runIds = runIdsByRepository.get(workflow.repository) || new Set(); + const runRecords = new Map((workflow.runHealth?.runRecords || []).map((run) => [Number(run.runId), run])); + for (const runId of workflow.runHealth?.runIds || []) { + runIds.add(runId); + workflowByRun.set(`${workflow.repository}:${runId}`, { workflow, run: runRecords.get(Number(runId)) || null }); + } + runIdsByRepository.set(workflow.repository, runIds); + } + + const runs = new Map(); + const temporaryRoot = configuredCacheRoot || await mkdtemp(path.join(os.tmpdir(), "pages-aic-")); + await mkdir(temporaryRoot, { recursive: true }); + try { + const repositories = await mapWithConcurrency([...runIdsByRepository], concurrency, async ([repository, runIds]) => { + if (runIds.size === 0) { + return { repository, selectedRuns: 0, reportedRuns: 0, available: true, complete: true }; + } + try { + const stdout = await runGhAw(repository, runIds, path.join(temporaryRoot, repository.replace("/", "-"))); + const result = JSON.parse(stdout); + let reportedRuns = 0; + for (const run of result.runs || []) { + const runId = Number(run.database_id ?? run.run_id ?? run.id); + const aic = Number(run.aic); + if (!Number.isFinite(runId) || !Number.isFinite(aic)) continue; + const metadata = workflowByRun.get(`${repository}:${runId}`); + const mode = metadata?.run?.displayTitle?.match(/(?:^|\s[·|:-]\s)(preview|staged|review|live)$/i)?.[1]?.toLowerCase() || null; + runs.set(`${repository}:${runId}`, { + repository, + runId, + workflowName: run.workflow_name || run.workflow || metadata?.workflow?.name || null, + workflowPath: metadata?.workflow?.path || null, + mode: mode === "preview" ? "staged" : mode, + conclusion: metadata?.run?.conclusion || null, + createdAt: run.created_at || run.started_at || metadata?.run?.createdAt || null, + aic, + }); + reportedRuns += 1; + } + return { + repository, + selectedRuns: runIds.size, + reportedRuns, + available: true, + complete: reportedRuns === runIds.size, + }; + } catch (error) { + console.warn(`AI Credit usage unavailable for ${repository}: ${error.message}`); + return { repository, selectedRuns: runIds.size, reportedRuns: 0, available: false, complete: false }; + } + }); + + const usage = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + windowStart: inventory.runHealth?.windowStart || null, + windowHours: inventory.runHealth?.windowHours || null, + available: repositories.every((entry) => entry.available), + complete: repositories.every((entry) => entry.complete), + repositories, + runs: [...runs.values()], + }; + await mkdir(path.dirname(outputPath), { recursive: true }); + await writeFile(outputPath, `${JSON.stringify(usage, null, 2)}\n`); + console.log(`Collected ${usage.runs.length} AIC-bearing runs with concurrency ${concurrency}; coverage ${usage.complete ? "complete" : "partial"}`); + } finally { + if (!configuredCacheRoot) await rm(temporaryRoot, { recursive: true, force: true }); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/.github/skills/github-pages-report/deployed-workflows.mjs b/.github/scripts/pages-report/deployed-workflows.mjs similarity index 54% rename from .github/skills/github-pages-report/deployed-workflows.mjs rename to .github/scripts/pages-report/deployed-workflows.mjs index 0875d59..823701e 100644 --- a/.github/skills/github-pages-report/deployed-workflows.mjs +++ b/.github/scripts/pages-report/deployed-workflows.mjs @@ -1,26 +1,38 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; +(async () => { + const repository = process.env.GITHUB_REPOSITORY || ""; const organization = process.env.REPORT_ORGANIZATION || repository.split("/")[0]; const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ""; +const pagesToken = process.env.REPORT_PAGES_TOKEN || token; const outputPath = path.resolve(process.env.REPORT_DEPLOYED_WORKFLOWS || "_inventory/deployed-workflows.json"); const includePrivate = process.env.REPORT_INCLUDE_PRIVATE === "true"; const runWindowHours = Number(process.env.REPORT_RUN_WINDOW_HOURS || 24); const auditMaxPages = Number(process.env.REPORT_AUDIT_MAX_PAGES || 100); const maxRetryDelayMs = Number(process.env.REPORT_MAX_RETRY_SECONDS || 30) * 1000; +const allowedRepositories = [...new Set((process.env.REPORT_ALLOWED_REPOS || "").split(",") + .map((value) => value.trim().toLowerCase()).filter(Boolean))]; +const repositoryScopeEnabled = allowedRepositories.length > 0; if (!organization || !token) throw new Error("GITHUB_REPOSITORY (or REPORT_ORGANIZATION) and GITHUB_TOKEN are required"); +if (allowedRepositories.some((value) => !/^[a-z0-9][a-z0-9-]*\/[a-z0-9._-]+$/.test(value))) { + throw new Error("REPORT_ALLOWED_REPOS must contain comma-separated owner/repository values"); +} -const headers = { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${token}`, - "User-Agent": "central-agentic-pages", - "X-GitHub-Api-Version": "2022-11-28", -}; +function markdownSourceUrl(lockUrl = "") { + if (!lockUrl.endsWith(".lock.yml")) return lockUrl; + return `${lockUrl.slice(0, -".lock.yml".length)}.md?plain=1`; +} -async function github(url, attempt = 0) { - const response = await fetch(`https://api.github.com${url}`, { headers }); +async function github(url, attempt = 0, authToken = token) { + const response = await fetch(`https://api.github.com${url}`, { headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${authToken}`, + "User-Agent": "central-agentic-pages", + "X-GitHub-Api-Version": "2022-11-28", + } }); if (response.ok) return { body: await response.json(), headers: response.headers }; if ((response.status === 403 || response.status === 429) && attempt < 3) { const retryAfter = Number(response.headers.get("retry-after")); @@ -33,11 +45,22 @@ async function github(url, attempt = 0) { } console.warn(`GitHub API ${response.status}; retrying ${url} in ${Math.ceil(delay / 1000)} seconds`); await new Promise((resolve) => setTimeout(resolve, delay)); - return github(url, attempt + 1); + return github(url, attempt + 1, authToken); } throw new Error(`GitHub API ${response.status} for ${url}`); } +async function requirePrivatePages() { + if (!includePrivate) return; + if (!repository) throw new Error("GITHUB_REPOSITORY is required to verify private Pages access"); + const pages = (await github(`/repos/${repository}/pages`, 0, pagesToken)).body; + if (pages.public !== false) { + throw new Error(`Refusing to discover private repository data because GitHub Pages for ${repository} is not private`); + } +} + +await requirePrivatePages(); + function searchQuery(minimum, maximum) { return `org:${organization} path:.github/workflows extension:yml "generated by gh-aw" size:${minimum}..${maximum}`; } @@ -110,11 +133,46 @@ async function repositoryMetadata(repositoryName) { } } +async function organizationRepositorySummary() { + if (repositoryScopeEnabled) return { public: null, private: null, internal: null, total: null }; + try { + async function count(type) { + const response = await github(`/orgs/${organization}/repos?type=${type}&per_page=1&page=1`); + const lastPage = response.headers.get("link")?.match(/[?&]page=(\d+)>; rel="last"/)?.[1]; + return lastPage ? Number(lastPage) : response.body.length; + } + const [total, publicRepositories, privateRepositories] = await Promise.all([ + count("all"), + count("public"), + count("private"), + ]); + return { + public: publicRepositories, + private: privateRepositories, + internal: total - publicRepositories - privateRepositories, + total, + }; + } catch (error) { + console.warn(`${error.message}; organization repository totals will be unavailable`); + return { public: null, private: null, internal: null, total: null }; + } +} + async function fileContent(repositoryName, filePath) { const response = await github(`/repos/${repositoryName}/contents/${encodeURIComponent(filePath).replaceAll("%2F", "/")}`); return Buffer.from(response.body.content || "", "base64").toString("utf8"); } +async function repositoryManifestFiles(repositoryName) { + const metadata = await repositoryMetadata(repositoryName); + if (!metadata.default_branch || (metadata.private && !includePrivate && repositoryName !== repository)) return []; + const tree = (await github(`/repos/${repositoryName}/git/trees/${encodeURIComponent(metadata.default_branch)}?recursive=1`)).body.tree || []; + return tree.filter((item) => item.type === "blob" && item.path.split("/").at(-1) === "aw.yml").map((item) => ({ + path: item.path, + repository: metadata, + })); +} + function manifestScalar(source, key) { const value = source.match(new RegExp(`^${key}:[ \\t]*(.+)$`, "m"))?.[1]?.trim() || ""; return value.replace(/^['"]|['"]$/g, ""); @@ -132,54 +190,78 @@ function nextPagePath(headers) { return headers.get("link")?.match(/]+)>; rel="next"/)?.[1] || ""; } -async function collectRunHealth(workflowIds) { +async function collectRunHealth(registryByRepository) { const windowStart = new Date(Date.now() - runWindowHours * 60 * 60 * 1000); - const searchStart = new Date(windowStart); - searchStart.setUTCDate(searchStart.getUTCDate() - 1); - const phrase = `action:workflows.completed_workflow_run created:>=${searchStart.toISOString().slice(0, 10)}`; - let nextPath = `/orgs/${organization}/audit-log?phrase=${encodeURIComponent(phrase)}&include=all&order=desc&per_page=100`; let page = 0; let complete = true; + let available = true; const totals = new Map(); - try { - while (nextPath && page < auditMaxPages) { - const response = await github(nextPath); - page += 1; - for (const event of response.body || []) { - if (!workflowIds.has(event.workflow_id) || Number(event.created_at) < windowStart.getTime()) continue; - const current = totals.get(event.workflow_id) || { runs: 0, successful: 0, failed: 0, cancelled: 0, other: 0 }; - current.runs += 1; - if (event.conclusion === "success") current.successful += 1; - else if (["failure", "timed_out", "startup_failure", "action_required"].includes(event.conclusion)) current.failed += 1; - else if (event.conclusion === "cancelled") current.cancelled += 1; - else current.other += 1; - totals.set(event.workflow_id, current); + await mapWithConcurrency([...registryByRepository], 4, async ([repositoryName, registry]) => { + const workflowIds = new Set([...registry.values()].map((workflow) => workflow.id)); + try { + for (let repositoryPage = 1; repositoryPage <= auditMaxPages; repositoryPage += 1) { + const response = await github(`/repos/${repositoryName}/actions/runs?created=${encodeURIComponent(`>=${windowStart.toISOString()}`)}&per_page=100&page=${repositoryPage}`); + const runs = response.body.workflow_runs || []; + page += 1; + for (const run of runs) { + if (!workflowIds.has(run.workflow_id)) continue; + const current = totals.get(run.workflow_id) || { runs: 0, successful: 0, failed: 0, cancelled: 0, skipped: 0, pending: 0, other: 0, runIds: [], runRecords: [] }; + current.runIds.push(run.id); + current.runRecords.push({ + runId: run.id, + conclusion: run.conclusion, + status: run.status, + createdAt: run.created_at, + displayTitle: run.display_title, + }); + current.runs += 1; + if (run.conclusion === "success") current.successful += 1; + else if (["failure", "timed_out", "startup_failure", "action_required"].includes(run.conclusion)) current.failed += 1; + else if (run.conclusion === "cancelled") current.cancelled += 1; + else if (run.conclusion === "skipped") current.skipped += 1; + else if (run.conclusion === null) current.pending += 1; + else current.other += 1; + totals.set(run.workflow_id, current); + } + if (runs.length < 100) break; + if (repositoryPage === auditMaxPages) complete = false; } - nextPath = nextPagePath(response.headers); + } catch (error) { + available = false; + complete = false; + console.warn(`${error.message}; run health will be unavailable for ${repositoryName}`); } - if (nextPath) complete = false; - } catch (error) { - console.warn(`${error.message}; run health will be unavailable`); - return { available: false, complete: false, windowStart: windowStart.toISOString(), pages: page, totals }; - } - return { available: true, complete, windowStart: windowStart.toISOString(), pages: page, totals }; + }); + return { available, complete, windowStart: windowStart.toISOString(), pages: page, totals }; } let matches = []; let manifestMatches = []; let workflowSearchAvailable = true; let manifestSearchAvailable = true; -try { - matches = await searchPartition(0, 499999); -} catch (error) { - workflowSearchAvailable = false; - console.warn(`${error.message}; organization workflow search will be unavailable`); -} -try { - manifestMatches = await searchCode(`org:${organization} filename:aw.yml`); -} catch (error) { - manifestSearchAvailable = false; - console.warn(`${error.message}; organization bundle search will be unavailable`); +if (!repositoryScopeEnabled) { + try { + matches = await searchPartition(0, 499999); + } catch (error) { + workflowSearchAvailable = false; + console.warn(`${error.message}; organization workflow search will be unavailable`); + } + try { + manifestMatches = await searchCode(`org:${organization} filename:aw.yml`); + } catch (error) { + manifestSearchAvailable = false; + console.warn(`${error.message}; organization bundle search will be unavailable`); + } +} else { + manifestMatches = (await mapWithConcurrency([repository, ...allowedRepositories], 8, async (repositoryName) => { + try { + return await repositoryManifestFiles(repositoryName); + } catch (error) { + manifestSearchAvailable = false; + console.warn(`${error.message}; operation manifest discovery will be unavailable for ${repositoryName}`); + return []; + } + })).flat(); } const discovered = new Map(); for (const item of matches) { @@ -194,25 +276,26 @@ for (const item of matches) { } const manifestFiles = manifestMatches.filter((item) => item.path.split("/").at(-1) === "aw.yml" && (includePrivate || !item.repository.private)); -const repositoryNames = [...new Set([ - repository, - ...[...discovered.values()].map((item) => item.repository), - ...manifestFiles.map((item) => item.repository.full_name), +const repositoryNames = [...new Set(repositoryScopeEnabled ? [repository, ...allowedRepositories] : [ + repository, ...[...discovered.values()].map((item) => item.repository), ...manifestFiles.map((item) => item.repository.full_name), ])].sort(); const registryByRepository = new Map((await mapWithConcurrency(repositoryNames, 8, async (repositoryName) => [ repositoryName, await registeredWorkflows(repositoryName), ])).filter(Boolean)); -const hostMetadata = await repositoryMetadata(repository); -for (const workflow of registryByRepository.get(repository)?.values() || []) { - if (!workflow.path.startsWith(".github/workflows/") || !workflow.path.endsWith(".lock.yml")) continue; - discovered.set(`${repository}:${workflow.path}`, { - repository, - visibility: hostMetadata.visibility?.toLowerCase() || (hostMetadata.private ? "private" : "public"), - path: workflow.path, - sourceUrl: workflow.html_url, - }); +for (const repositoryName of repositoryNames) { + const metadata = await repositoryMetadata(repositoryName); + if (metadata.private && !includePrivate && repositoryName !== repository) continue; + for (const workflow of registryByRepository.get(repositoryName)?.values() || []) { + if (!workflow.path.startsWith(".github/workflows/") || !workflow.path.endsWith(".lock.yml")) continue; + discovered.set(`${repositoryName}:${workflow.path}`, { + repository: repositoryName, + visibility: metadata.visibility?.toLowerCase() || (metadata.private ? "private" : "public"), + path: workflow.path, + sourceUrl: workflow.html_url, + }); + } } const bundles = (await mapWithConcurrency(manifestFiles, 8, async (item) => { @@ -244,8 +327,10 @@ const bundles = (await mapWithConcurrency(manifestFiles, 8, async (item) => { } })).filter(Boolean).sort((left, right) => left.repository.localeCompare(right.repository) || left.name.localeCompare(right.name)); -const registeredWorkflowIds = new Set([...registryByRepository.values()].flatMap((registry) => [...registry.values()].map((workflow) => workflow.id))); -const runHealth = await collectRunHealth(registeredWorkflowIds); +const [runHealth, organizationRepositories] = await Promise.all([ + collectRunHealth(registryByRepository), + organizationRepositorySummary(), +]); const workflows = [...discovered.values()].map((item) => { const registered = registryByRepository.get(item.repository)?.get(item.path); @@ -254,19 +339,24 @@ const workflows = [...discovered.values()].map((item) => { id: registered?.id || null, name: registered?.name || item.path.split("/").at(-1).replace(/\.lock\.yml$/, ""), state: registered?.state || "unknown", - htmlUrl: registered?.html_url || `https://github.com/${item.repository}/actions`, + htmlUrl: markdownSourceUrl(registered?.html_url || item.sourceUrl) || `https://github.com/${item.repository}/actions`, createdAt: registered?.created_at || null, updatedAt: registered?.updated_at || null, - runHealth: registered ? runHealth.totals.get(registered.id) || { runs: 0, successful: 0, failed: 0, cancelled: 0, other: 0 } : null, + runHealth: registered ? runHealth.totals.get(registered.id) || { runs: 0, successful: 0, failed: 0, cancelled: 0, skipped: 0, pending: 0, other: 0, runIds: [], runRecords: [] } : null, }; }).sort((left, right) => left.repository.localeCompare(right.repository) || left.name.localeCompare(right.name)); +const operationWorkflowKeys = new Set(bundles.flatMap((bundle) => bundle.workflows.map((workflow) => `${bundle.repository}:${workflow.lockPath}`))); +const standaloneWorkflows = workflows.filter((workflow) => !operationWorkflowKeys.has(`${workflow.repository}:${workflow.path}`)); const inventory = { schemaVersion: 1, generatedAt: new Date().toISOString(), organization, + repositoryScope: repositoryScopeEnabled ? "allowlist" : "organization", + allowedRepositories, includePrivate, repositoryCount: repositoryNames.length, + organizationRepositories, discovery: { workflowSearchAvailable, manifestSearchAvailable, @@ -280,9 +370,14 @@ const inventory = { pages: runHealth.pages, }, bundles, + standaloneWorkflows, workflows, }; await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${JSON.stringify(inventory, null, 2)}\n`); -console.log(`Discovered ${bundles.length} bundles and ${workflows.length} compiled agentic workflows across ${repositoryNames.length} repositories; run health ${runHealth.available ? runHealth.complete ? "complete" : "partial" : "unavailable"}`); +console.log(`Discovered ${bundles.length} operations and ${standaloneWorkflows.length} standalone workflows across ${repositoryNames.length} repositories; run health ${runHealth.available ? runHealth.complete ? "complete" : "partial" : "unavailable"}`); +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/.github/skills/github-pages-report/inventory.mjs b/.github/scripts/pages-report/inventory.mjs similarity index 90% rename from .github/skills/github-pages-report/inventory.mjs rename to .github/scripts/pages-report/inventory.mjs index 5802ce5..f177bd8 100644 --- a/.github/skills/github-pages-report/inventory.mjs +++ b/.github/scripts/pages-report/inventory.mjs @@ -2,6 +2,8 @@ import { mkdir, writeFile } from "node:fs/promises"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import path from "node:path"; +(async () => { + const root = path.resolve(process.env.REPORT_ROOT || "."); const outputPath = path.resolve(process.env.REPORT_INVENTORY || "_inventory/control-plane.json"); const workflowDirectory = path.join(root, ".github/workflows"); @@ -24,7 +26,7 @@ function inlineList(source, key) { } function rolloutModeVariable(source) { - return source.match(/rollout_mode:\s*\$\{\{\s*vars\.([A-Z0-9_]+)/)?.[1] || ""; + return source.match(/(?:rollout_mode|CENTRAL_AGENTIC_OPS_MODE):\s*\$\{\{\s*vars\.([A-Z0-9_]+)/)?.[1] || ""; } function manifestIncludes(source) { @@ -78,6 +80,7 @@ function discoverInventory() { const source = readFileSync(path.join(workflowDirectory, entry.name), "utf8"); const stem = entry.name.slice(0, -3); const role = source.match(/uses:\s+shared\/control\.md[\s\S]*?role:\s+(orchestrator|worker)/)?.[1] || "standalone"; + const maxAiCredits = Number(scalar(source, "max-ai-credits")); return { id: stem, name: scalar(source, "name") || stem, @@ -85,6 +88,7 @@ function discoverInventory() { emoji: scalar(source, "emoji"), trackerId: scalar(source, "tracker-id"), role, + maxAiCredits: Number.isFinite(maxAiCredits) && maxAiCredits > 0 ? maxAiCredits : null, rolloutModeVariable: role === "orchestrator" ? rolloutModeVariable(source) : "", sourcePath, lockPath: `.github/workflows/${stem}.lock.yml`, @@ -100,6 +104,7 @@ function discoverInventory() { name: orchestrator.package?.name || orchestrator.name, description: orchestrator.package?.description || orchestrator.description, workflow: orchestrator.sourcePath, + maxAiCredits: orchestrator.maxAiCredits, rolloutModeVariable: orchestrator.rolloutModeVariable, compiled: orchestrator.compiled, workers: orchestrator.workers.map((workerId) => workflowById.get(workerId)).filter(Boolean), @@ -116,4 +121,8 @@ function discoverInventory() { const inventory = discoverInventory(); await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${JSON.stringify(inventory, null, 2)}\n`); -console.log(`Discovered ${inventory.bundles.length} bundles and ${inventory.standalone.length} standalone workflows in ${outputPath}`); \ No newline at end of file +console.log(`Discovered ${inventory.bundles.length} operations and ${inventory.standalone.length} standalone workflows in ${outputPath}`); +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/.github/skills/github-pages-report/report.mjs b/.github/scripts/pages-report/report.mjs similarity index 54% rename from .github/skills/github-pages-report/report.mjs rename to .github/scripts/pages-report/report.mjs index d09584b..e400c59 100644 --- a/.github/skills/github-pages-report/report.mjs +++ b/.github/scripts/pages-report/report.mjs @@ -2,12 +2,16 @@ import { copyFile, mkdir, readdir, writeFile } from "node:fs/promises"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; +(async () => { + const repository = process.env.GITHUB_REPOSITORY; const token = process.env.GITHUB_TOKEN; +const pagesToken = process.env.REPORT_PAGES_TOKEN || token; const outputDirectory = process.env.REPORT_OUTPUT || "_site"; const inventoryPath = process.env.REPORT_INVENTORY; const valueReportRoot = process.env.REPORT_VALUE_ROOT || ".github/value"; const deployedWorkflowsPath = process.env.REPORT_DEPLOYED_WORKFLOWS || "_inventory/deployed-workflows.json"; +const aicUsagePath = process.env.REPORT_AIC_USAGE || "_inventory/aic-usage.json"; if (!repository || !token || !inventoryPath) { throw new Error("GITHUB_REPOSITORY, GITHUB_TOKEN, and REPORT_INVENTORY are required"); @@ -20,11 +24,17 @@ const inventory = JSON.parse(readFileSync(inventoryPath, "utf8")); const deployedInventory = existsSync(deployedWorkflowsPath) ? JSON.parse(readFileSync(deployedWorkflowsPath, "utf8")) : { schemaVersion: 1, organization: owner, repositoryCount: 0, bundles: [], workflows: [] }; +const aicUsage = existsSync(aicUsagePath) + ? JSON.parse(readFileSync(aicUsagePath, "utf8")) + : { schemaVersion: 1, available: false, complete: false, repositories: [], runs: [] }; +const allowedRepositories = new Set((process.env.REPORT_ALLOWED_REPOS || "").split(",") + .map((value) => value.trim().toLowerCase()).filter(Boolean)); if (inventory.schemaVersion !== 1 || !Array.isArray(inventory.workflows) || !Array.isArray(inventory.bundles)) { throw new Error(`Unsupported or invalid control-plane inventory: ${inventoryPath}`); } const bundleDefinitions = inventory.bundles; const standaloneDefinitions = inventory.standalone; +const workflowDefinitionById = new Map(inventory.workflows.map((workflow) => [workflow.id, workflow])); const workerDefinitions = bundleDefinitions.flatMap((bundle) => bundle.workers.map((worker) => ({ ...worker, bundleId: bundle.id, bundleName: bundle.name }))); const workerIds = new Set(workerDefinitions.map((worker) => worker.id)); @@ -56,11 +66,11 @@ const reportDefinitions = [ ...standaloneDefinitions.map((workflow) => ({ ...workflow, workers: [], missingWorkers: [] })), ]; -async function github(pathname) { +async function github(pathname, authToken = token) { const response = await fetch(`${apiRoot}${pathname}`, { headers: { Accept: "application/vnd.github.full+json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, "X-GitHub-Api-Version": "2022-11-28", }, }); @@ -77,6 +87,19 @@ async function githubOptional(pathname, fallback) { } } +async function requirePrivatePages() { + const hasPrivateData = deployedInventory.includePrivate === true + || (deployedInventory.workflows || []).some((workflow) => workflow.visibility === "private") + || (deployedInventory.bundles || []).some((bundle) => bundle.visibility === "private"); + if (!hasPrivateData) return; + const pages = await github(`/repos/${owner}/${repo}/pages`, pagesToken); + if (pages.public !== false) { + throw new Error(`Refusing to publish private repository data because GitHub Pages for ${repository} is not private`); + } +} + +await requirePrivatePages(); + async function githubPages(pathname, maxPages = 10) { const separator = pathname.includes("?") ? "&" : "?"; const items = []; @@ -100,13 +123,17 @@ function bundleFor(...values) { function workflowFrom(body = "") { const heading = body.match(/^###\s+(.+)$/m)?.[1]?.trim(); const provenance = body.match(/Generated from \[([^\]]+)\]\([^)]*\/actions\/runs\/\d+\)/)?.[1]; - return provenance || heading || "Agentic workflow"; + return provenance || heading || "GitHub Agentic Workflow"; } function runUrlFrom(body = "") { return body.match(/https:\/\/github\.com\/[^\s)]+\/actions\/runs\/\d+/)?.[0] || ""; } +function repositoryFrom(body = "") { + return body.match(/(?:target repository|target repo):\s*`?([a-z0-9][a-z0-9-]*\/[a-z0-9._-]+)/i)?.[1] || ""; +} + function markerFrom(body = "", marker) { return body.match(new RegExp(``, "i"))?.[1]?.trim() || ""; } @@ -169,9 +196,10 @@ function formatDay(value) { } function recordFromIssue(issue) { - const workflow = workflowFrom(issue.body || ""); - const generatedSafeOutput = /Generated (?:from|with) \[[^\]]+\]\([^)]*\/actions\/runs\/\d+\)/.test(issue.body || ""); - const bundle = bundleFor(issue.title, workflow, issue.body); + const body = issue.body || ""; + const workflow = workflowFrom(body); + const generatedSafeOutput = /Generated (?:from|with) \[[^\]]+\]\([^)]*\/actions\/runs\/\d+\)/.test(body); + const bundle = bundleFor(issue.title, workflow, body); const generatedSafeOutputTitle = /^\[[^\]]+\]\s/.test(issue.title) && bundle; if (!generatedSafeOutputTitle && !generatedSafeOutput) return null; if (!bundle || issue.title === "[aw] No-Op Runs") return null; @@ -187,10 +215,11 @@ function recordFromIssue(issue) { createdAt: issue.created_at, updatedAt: issue.updated_at, workflow, - runUrl: runUrlFrom(issue.body), - bundleId: markerFrom(issue.body, "bundle"), - correlationId: markerFrom(issue.body, "correlation"), - aic: aicFrom(issue.body), + runUrl: runUrlFrom(body), + repository: repositoryFrom(body), + bundleId: markerFrom(body, "bundle"), + correlationId: markerFrom(body, "correlation"), + aic: aicFrom(body), warning: hasReportWarning(issue.body_html), }; } @@ -286,7 +315,7 @@ function outcomeListing(recordsForPage) { `; } -function findingsListing(recordsForPage) { +function findingsListing(recordsForPage, { showMode = false, emptyMessage = "No reports have been recorded for this mode." } = {}) { const open = recordsForPage.filter((record) => ["open", "available", "published"].includes(record.state)).length; const resolved = recordsForPage.length - open; const rows = recordsForPage.map((record) => `
@@ -296,18 +325,18 @@ function findingsListing(recordsForPage) {

${escapeHtml(record.summary || "No report summary was provided.")}

${escapeHtml(record.state)} - ${escapeHtml(record.workflow)} + ${showMode ? `${escapeHtml(record.mode)}` : ""} ${escapeHtml(record.kind.replaceAll("-", " "))}
`).join("\n"); - return `
+ return `

Reports

${open} Open ${resolved} Resolved
- -
${rows || '

No reports have been recorded for this mode.

'}
+ +
${rows || `

${escapeHtml(emptyMessage)}

`}
`; } @@ -319,18 +348,56 @@ function modeSummary(recordsForBundle, mode) { function modeTabs(bundle, selectedMode) { const tabs = [ - ["staged", "Staged", "No writes"], ["review", "Review", "Proposals"], ["live", "Live", "Production"], ]; return ``; } +function overviewModeTabs(selectedMode) { + const tabs = [ + ["review", "Review", "Proposals", "review.html"], + ["live", "Live", "Production", "live.html"], + ]; + return ``; +} + +function bundleTabs(bundle, selectedView) { + const tabs = [ + ["reports", "Reports", "issue", `../operations/${bundle.id}.html`], + ["insights", "Insights", "graph", `../insights/${bundle.id}.html`], + ]; + return ``; +} + +function repositoryTabs(repositoryName, selectedView) { + const pageName = repositoryPageName(repositoryName); + const tabs = [ + ["reports", "Reports", "issue", `${pageName}.html`], + ["insights", "Insights", "graph", `${pageName}-insights.html`], + ]; + return ``; +} + function configuredModeFor(bundle) { const mode = repositoryVariables.get(bundle.rolloutModeVariable) || "staged"; return normalizeMode(mode) === "unknown" ? "staged" : normalizeMode(mode); } +function repositoryVariablesFromEnvironment() { + const source = process.env.REPORT_REPOSITORY_VARIABLES || "{}"; + let values; + try { + values = JSON.parse(source); + } catch { + throw new Error("REPORT_REPOSITORY_VARIABLES must be valid JSON"); + } + if (!values || Array.isArray(values) || typeof values !== "object") { + throw new Error("REPORT_REPOSITORY_VARIABLES must be a JSON object"); + } + return Object.entries(values).map(([name, value]) => [name, String(value)]); +} + function modeIndicator(mode) { const icons = { staged: "eye", review: "beaker", live: "rocket" }; const label = `${mode[0].toUpperCase()}${mode.slice(1)}`; @@ -341,25 +408,32 @@ function octicon(name, className = "") { return ``; } +function agenticWorkflowMark() { + return ``; +} + function octiconSprite() { return ``; } -function layout({ title, description, content, nested = false, navigation = "", configuredMode = "", overviewMode = "", campaignType = "", activeSection = "", activeBundle = "" }) { +function layout({ title, description, content, nested = false, navigation = "", configuredMode = "", overviewMode = "", activeSection = "", activeBundle = "" }) { const root = nested ? "../" : "./"; const stylesheetLink = `<${"link"} rel="stylesheet" href="${root}styles.css">`; - const overviewCurrent = nested || campaignType || activeSection ? "" : ' aria-current="page"'; - const campaignsCurrent = campaignType ? ' aria-current="page"' : ""; - const insightLinks = bundleDefinitions.map((bundle) => { - const current = activeSection === "insights" && activeBundle === bundle.id ? ' aria-current="page"' : ""; - const icon = bundle.id.includes("dependabot") ? "dependabot" : "graph"; - return `${octicon(icon)}${escapeHtml(bundle.name)}`; - }).join("\n"); - const findingLinks = bundleDefinitions.map((bundle) => { - const current = activeSection === "findings" && activeBundle === bundle.id ? ' aria-current="page"' : ""; - const icon = bundle.id.includes("dependabot") ? "dependabot" : "codescan"; - return `${octicon(icon)}${escapeHtml(bundle.name)}`; + const operationsHref = `${root}operations/index.html`; + const overviewCurrent = activeSection === "overview" ? ' aria-current="page"' : ""; + const repositoriesCurrent = activeSection === "repositories" ? ' aria-current="page"' : ""; + const operationsCurrent = activeSection === "operations" ? ' aria-current="page"' : ""; + const workflowsCurrent = activeSection === "workflows" ? ' aria-current="page"' : ""; + const bundleLinks = bundleDefinitions.map((bundle) => { + const current = activeBundle === bundle.id ? ' aria-current="page"' : ""; + const icon = bundle.id.includes("dependabot") ? "dependabot" : "meter"; + return `${octicon(icon)}${escapeHtml(bundle.name)}`; }).join("\n"); + const freshness = ``; + const repositoryLink = `${octicon("mark-github")}`; + const reportActions = `
${freshness}${repositoryLink}
`; + const topNavigation = navigation + ? navigation.replace("", `${reportActions}`) + : ``; return ` @@ -396,40 +474,35 @@ function layout({ title, description, content, nested = false, navigation = "",
- ${navigation} + ${topNavigation}

${escapeHtml(title)}

${configuredMode ? modeIndicator(configuredMode) : ""}

${escapeHtml(description)}

- ${nested ? `

Last updated ${escapeHtml(formatDate(generatedAt))}

` : ""}
- ${campaignType || activeSection === "workflows" ? "" : `
-
${octicon("issue")}Filter3mode:staged mode:review mode:live
- ${overviewMode ? "Last 30 days" : "All recorded"} + ${activeBundle || ["overview", "workflows", "repositories"].includes(activeSection) ? "" : `
+ ${overviewMode ? "" : `
${octicon("issue")}Filter2mode:review mode:live
`} + ${overviewMode ? "" : 'All recorded'} Export JSON
`} - ${nested || campaignType || activeSection === "workflows" ? "" : '

Results are based on the workflows and durable outputs available in this repository.

'} + ${overviewMode ? `

Managed operations from ${escapeHtml(repository)}.

` : nested || ["overview", "workflows", "repositories"].includes(activeSection) ? "" : '

Results are based on the workflows and durable outputs available in this repository.

'}
${content}
- +
Generated deterministically from GitHub repository data.
@@ -440,9 +513,14 @@ const [issues, comments, artifactResponse, variableResponse] = await Promise.all githubPages(`/repos/${owner}/${repo}/issues?state=all&sort=updated&direction=desc`), githubPages(`/repos/${owner}/${repo}/issues/comments?sort=updated&direction=desc`), github(`/repos/${owner}/${repo}/actions/artifacts?per_page=100`), - githubOptional(`/repos/${owner}/${repo}/actions/variables?per_page=100`, { variables: [] }), + process.env.REPORT_REPOSITORY_VARIABLES + ? Promise.resolve({ variables: [] }) + : githubOptional(`/repos/${owner}/${repo}/actions/variables?per_page=100`, { variables: [] }), +]); +const repositoryVariables = new Map([ + ...(variableResponse.variables || []).map((variable) => [variable.name, variable.value]), + ...repositoryVariablesFromEnvironment(), ]); -const repositoryVariables = new Map((variableResponse.variables || []).map((variable) => [variable.name, variable.value])); const issueByUrl = new Map(issues.map((issue) => [issue.url, issue])); const runCache = new Map(); function normalizeMode(mode) { @@ -460,7 +538,10 @@ async function metadataFromRunUrl(runUrl) { } const run = await runCache.get(cacheKey); const mode = run?.display_title?.match(/(?:^|\s[·|:-]\s)(preview|staged|review|live)$/i)?.[1]?.toLowerCase(); - const targetRepository = run?.display_title?.match(/\b([a-z0-9][a-z0-9-]*\/[a-z0-9._-]+)\b/i)?.[1]; + const repositoryCandidates = [...(run?.display_title || "").matchAll(/\b([a-z0-9][a-z0-9-]*\/[a-z0-9._-]+)\b/gi)].map((candidate) => candidate[1]); + const targetRepository = repositoryCandidates.find((candidate) => allowedRepositories.size === 0 + ? candidate.split("/")[0].toLowerCase() === owner.toLowerCase() + : allowedRepositories.has(candidate.toLowerCase())); return { mode: normalizeMode(mode), conclusion: run?.conclusion || "unknown", @@ -485,24 +566,68 @@ const records = (await Promise.all(discoveredRecords.map(async (record) => { repository: record.repository || metadata.repository || "", }; }))).sort((left, right) => new Date(right.updatedAt) - new Date(left.updatedAt)); -const reportRecords = records.filter((record) => ["staged", "review", "live"].includes(record.mode)); +const scopedRecords = allowedRepositories.size === 0 + ? records + : records.filter((record) => allowedRepositories.has(record.repository.toLowerCase())); +const reportRecords = scopedRecords.filter((record) => ["staged", "review", "live"].includes(record.mode)); await mkdir(outputDirectory, { recursive: true }); await writeFile(path.join(outputDirectory, "inventory.json"), `${JSON.stringify(inventory, null, 2)}\n`); -await writeFile(path.join(outputDirectory, "records.json"), `${JSON.stringify({ generatedAt, repository, inventory, records }, null, 2)}\n`); +await writeFile(path.join(outputDirectory, "records.json"), `${JSON.stringify({ generatedAt, repository, inventory, records: scopedRecords }, null, 2)}\n`); + +const failedConclusions = new Set(["action_required", "failure", "stale", "startup_failure", "timed_out"]); +function isFailureRecord(record) { + return failedConclusions.has(record.conclusion) || /\b(?:failed jobs?|workflow failure|workflow .+ failed)\b/i.test(`${record.title} ${record.summary}`); +} + +function collectRuns(recordsForMode) { + const runs = new Map(); + for (const record of recordsForMode) { + if (!record.runUrl) continue; + const run = runs.get(record.runUrl) || { conclusion: "unknown", failed: false, warning: false, aic: null, createdAt: record.createdAt, repository: record.repository }; + if (record.conclusion !== "unknown") run.conclusion = record.conclusion; + run.failed ||= isFailureRecord(record); + run.warning ||= record.warning; + if (new Date(record.createdAt) < new Date(run.createdAt)) run.createdAt = record.createdAt; + if (Number.isFinite(record.aic)) run.aic = Math.max(run.aic || 0, record.aic); + runs.set(record.runUrl, run); + } + return [...runs.values()]; +} + +function runStatus(run) { + if (run.conclusion === "cancelled") return "cancelled"; + if (run.failed) return "failed"; + if (run.conclusion === "success") return "successful"; + return "other"; +} + +function summarizeRuns(recordsForMode) { + const values = collectRuns(recordsForMode); + return { + total: values.length, + successful: values.filter((run) => run.conclusion === "success" && !run.failed).length, + failed: values.filter((run) => run.failed).length, + warnings: values.filter((run) => run.warning).length, + other: values.filter((run) => run.conclusion !== "success" && !run.failed).length, + aic: values.reduce((total, run) => total + (run.aic || 0), 0), + aicRuns: values.filter((run) => run.aic !== null).length, + }; +} + +const modeLabels = { review: "Review", live: "Live" }; const trendDays = Array.from({ length: 30 }, (_, index) => { const date = new Date(generatedAt); date.setUTCHours(0, 0, 0, 0); date.setUTCDate(date.getUTCDate() - (29 - index)); return date; }); -const trendCounts = (recordsForMode) => trendDays.map((date) => { +const trendCounts = (runs) => trendDays.map((date) => { const endOfDay = new Date(date.getTime() + 86400000); - return recordsForMode.filter((record) => new Date(record.createdAt) < endOfDay).length; + return runs.filter((run) => new Date(run.createdAt) < endOfDay).length; }); const trendPoints = (values, maximum) => values.map((value, index) => `${58 + (index * 714 / 29)},${200 - (value * 150 / maximum)}`).join(" "); -const modeLabels = { live: "Live", review: "Review", staged: "Staged" }; function chartPoints(series, maximum) { return trendDays.map((day, index) => { @@ -517,9 +642,9 @@ function chartPoints(series, maximum) { `; }).join("\n"); @@ -535,7 +660,7 @@ function overviewTrend(mode, modeRecords) { const maximum = Math.max(1, ...series.successful, ...series.failed, ...series.cancelled); const label = modeLabels[mode]; return `
-

${label} runs over time

${runs.length}as of ${escapeHtml(formatDate(generatedAt))}

Group by: Status
+

${label} runs over time

${runs.length}as of ${escapeHtml(formatDate(generatedAt))}

Group by: Status
SuccessfulFailedCancelled
${label} runs by status over the last 30 days @@ -551,51 +676,6 @@ function overviewTrend(mode, modeRecords) {
`; } -const failedConclusions = new Set(["action_required", "failure", "stale", "startup_failure", "timed_out"]); - -function isFailureRecord(record) { - return failedConclusions.has(record.conclusion) || /\b(?:failed jobs?|workflow failure|workflow .+ failed)\b/i.test(`${record.title} ${record.summary}`); -} - -function collectRuns(recordsForMode) { - const runs = new Map(); - for (const record of recordsForMode) { - if (!record.runUrl) continue; - const run = runs.get(record.runUrl) || { conclusion: "unknown", failed: false, warning: false, aic: null, createdAt: record.createdAt, repository: record.repository }; - if (record.conclusion !== "unknown") run.conclusion = record.conclusion; - run.failed ||= isFailureRecord(record); - run.warning ||= record.warning; - if (new Date(record.createdAt) < new Date(run.createdAt)) run.createdAt = record.createdAt; - if (Number.isFinite(record.aic)) run.aic = Math.max(run.aic || 0, record.aic); - runs.set(record.runUrl, run); - } - return [...runs.values()]; -} - -function runStatus(run) { - if (run.conclusion === "cancelled") return "cancelled"; - if (run.failed) return "failed"; - if (run.conclusion === "success") return "successful"; - return "other"; -} - -function summarizeRuns(recordsForMode) { - const values = collectRuns(recordsForMode); - return { - total: values.length, - successful: values.filter((run) => run.conclusion === "success" && !run.failed).length, - failed: values.filter((run) => run.failed).length, - warnings: values.filter((run) => run.warning).length, - other: values.filter((run) => run.conclusion !== "success" && !run.failed).length, - aic: values.reduce((total, run) => total + (run.aic || 0), 0), - aicRuns: values.filter((run) => run.aic !== null).length, - }; -} - -function formatAic(value) { - return new Intl.NumberFormat("en", { maximumFractionDigits: 1 }).format(value); -} - function overviewMetrics(mode, modeRecords) { const runs = summarizeRuns(modeRecords); const definitions = [ @@ -603,11 +683,85 @@ function overviewMetrics(mode, modeRecords) { ["Failed runs", runs.failed, "Failed Actions conclusions and explicit failure reports"], ["Total AIC", formatAic(runs.aic), `Across ${runs.aicRuns} of ${runs.total} reported runs`], ]; - return `
-
+ return `
${definitions.map(([name, value, description]) => `
${name}
${value}

${description}

`).join("\n")} -
-
`; +
`; +} + +function bundleCapacityWorkflows(bundle) { + const orchestrator = workflowDefinitionById.get(bundle.id); + return [ + { + id: bundle.id, + name: orchestrator?.name || bundle.name, + path: orchestrator?.lockPath || bundle.workflow?.replace(/\.md$/, ".lock.yml"), + maxAiCredits: bundle.maxAiCredits || orchestrator?.maxAiCredits, + }, + ...bundle.workers.map((worker) => ({ + id: worker.id, + name: worker.name, + path: worker.lockPath, + maxAiCredits: worker.maxAiCredits, + })), + ].filter((workflow) => Number.isFinite(workflow.maxAiCredits) && workflow.maxAiCredits > 0); +} + +function bundleUtilization(mode, bundle) { + const workflows = bundleCapacityWorkflows(bundle); + const byPath = new Map(workflows.map((workflow) => [workflow.path, workflow])); + const byName = new Map(workflows.map((workflow) => [workflow.name.toLowerCase(), workflow])); + const coverage = (aicUsage.repositories || []).find((entry) => entry.repository === repository); + const utilization = { + available: coverage?.available ?? aicUsage.available, + complete: coverage?.complete ?? aicUsage.complete, + used: 0, + allowed: 0, + reportedRuns: 0, + completeAttemptAllowance: workflows.reduce((total, workflow) => total + workflow.maxAiCredits, 0), + }; + for (const run of aicUsage.runs || []) { + if (run.repository !== repository || run.mode !== mode) continue; + const workflow = byPath.get(run.workflowPath) || byName.get(String(run.workflowName || "").toLowerCase()); + if (!workflow) continue; + utilization.used += Number(run.aic) || 0; + utilization.allowed += workflow.maxAiCredits; + utilization.reportedRuns += 1; + } + utilization.ratio = utilization.allowed > 0 ? utilization.used / utilization.allowed : null; + return utilization; +} + +function bundleUtilizationPanel(mode) { + const windowHours = Number(aicUsage.windowHours); + const windowLabel = Number.isFinite(windowHours) && windowHours > 0 + ? `the last ${formatCount(windowHours)} hour${windowHours === 1 ? "" : "s"}` + : "the retained usage window"; + const cards = bundleDefinitions.map((bundle) => { + const utilization = bundleUtilization(mode, bundle); + const ratioPercent = utilization.ratio === null ? null : utilization.ratio * 100; + const meterPercent = ratioPercent === null ? 0 : Math.min(100, ratioPercent); + const status = ratioPercent === null ? "empty" : ratioPercent >= 80 ? "high" : ratioPercent >= 50 ? "medium" : "low"; + const value = !utilization.available || ratioPercent === null ? "—" : `${formatAic(ratioPercent)}%`; + const detail = !utilization.available + ? "AI Credit usage artifacts are unavailable." + : utilization.reportedRuns === 0 + ? "No completed runs in the retained window." + : `${formatAic(utilization.used)} of ${formatAic(utilization.allowed)} AIC across ${formatCount(utilization.reportedRuns)} reported run${utilization.reportedRuns === 1 ? "" : "s"}.`; + const coverageNote = utilization.available && !utilization.complete ? " Partial usage coverage." : ""; + const aria = ratioPercent === null + ? `${bundle.name}: no utilization available` + : `${bundle.name}: ${formatAic(utilization.used)} of ${formatAic(utilization.allowed)} AI Credits used, ${formatAic(ratioPercent)} percent`; + return `
+
${escapeHtml(bundle.name)}${escapeHtml(value)}
+ +

${escapeHtml(detail)}${escapeHtml(coverageNote)}

+ ${formatAic(utilization.completeAttemptAllowance)} AIC allowance per complete bundle attempt +
`; + }).join("\n"); + return `
+

Bundle AIC utilization

Actual AI Credits against summed per-run limits for ${escapeHtml(modeLabels[mode].toLowerCase())} operation runs retained from ${escapeHtml(windowLabel)}.

+
${cards}
+
`; } function overviewTable(mode, modeRecords) { @@ -616,168 +770,428 @@ function overviewTable(mode, modeRecords) { const latest = bundleRecords[0]; const runs = summarizeRuns(bundleRecords); const inventoryWarnings = (bundle.compiled ? 0 : 1) + bundle.missingWorkers.length; - return `${escapeHtml(bundle.name)}${runs.total}${runs.successful}${runs.failed}${runs.warnings}${inventoryWarnings}${formatAic(runs.aic)}${escapeHtml(latest ? formatDate(latest.updatedAt) : "No outputs yet")}`; + return `${escapeHtml(bundle.name)}${runs.total}${runs.successful}${runs.failed}${runs.warnings}${inventoryWarnings}${formatAic(runs.aic)}${escapeHtml(latest ? formatDate(latest.updatedAt) : "No outputs yet")}`; }).join("\n"); - return `
-

${modeLabels[mode]} output by bundle

-

Durable outputs and inventory health for each control-plane bundle.

-
- ${rows || ''}
${modeLabels[mode]} operational summary by bundle
BundleRunsSuccessfulFailedRun warningsInventory warningsAICLatest activity
No bundles discovered.
+ return `
+

${modeLabels[mode]} output by operation

+

Durable outputs and inventory health for each control-plane operation.

+
+ ${rows || ''}
${modeLabels[mode]} operational summary
OperationRunsSuccessfulFailedRun warningsInventory warningsAICLatest activity
No operations discovered.
`; } -function overviewContent(mode) { +function operationsOverviewContent(mode) { const windowStart = trendDays[0].getTime(); const modeRecords = reportRecords.filter((record) => record.mode === mode && new Date(record.createdAt).getTime() >= windowStart); - const tabs = ``; - return `${deployedWorkflowContent()}

Bundle activity

Control-plane bundle runs and durable outputs, grouped by rollout mode.

${tabs}${overviewTrend(mode, modeRecords)}${overviewMetrics(mode, modeRecords)}${overviewTable(mode, modeRecords)}`; + return `${overviewModeTabs(mode)}

Control-plane activity

Runs and durable outputs from operations managed by this repository.

${overviewMetrics(mode, modeRecords)}${overviewTable(mode, modeRecords)}${overviewTrend(mode, modeRecords)}${bundleUtilizationPanel(mode)}`; } -await writeFile(path.join(outputDirectory, "styles.css"), stylesheet()); -for (const [mode, filename] of [["live", "index.html"], ["review", "overview-review.html"], ["staged", "overview-staged.html"]]) { - await writeFile(path.join(outputDirectory, filename), layout({ - title: "Overview", - description: `${modeLabels[mode]} workflow trends and operational health across your organization.`, - content: overviewContent(mode), - overviewMode: mode, - })); +function formatAic(value) { + return new Intl.NumberFormat("en", { maximumFractionDigits: 1 }).format(value); } -const campaignCandidates = reportRecords.filter((record) => record.kind !== "noop" && ["open", "available", "published"].includes(record.state)); -const secretCampaignCandidates = campaignCandidates.filter((record) => /\bsecret(?:s| scanning)?\b/i.test(`${record.title} ${record.summary} ${record.workflow}`)); -const codeCampaignCandidates = campaignCandidates.filter((record) => !secretCampaignCandidates.includes(record)); - -function campaignContent(selectedType) { - const candidates = selectedType === "secrets" ? secretCampaignCandidates : codeCampaignCandidates; - const typeLabel = selectedType === "secrets" ? "Secrets" : "Code"; - const issueBody = `## Objective\n\nDescribe the improvement, remediation, or defined body of work.\n\n## Scope\n\nList the repositories, organizations, or other targets this campaign should coordinate across.\n\n## Time frame\n\n- Start date:\n- Target completion:\n\n## Agentic execution\n\nDescribe how agents should perform, track, and report the work.\n\n## Current signals\n\n${candidates.length} related ${typeLabel.toLowerCase()} output${candidates.length === 1 ? "" : "s"} currently available in the control plane.`; - const creationUrl = `https://github.com/${repository}/issues/new?title=${encodeURIComponent(`[campaign] ${typeLabel} initiative`)}&body=${encodeURIComponent(issueBody)}`; - return ` -
- ${octicon("goal", "campaign-empty-icon")} -

Start a new campaign

-

Launch a time-bound agentic initiative to improve, remediate, or complete defined ${typeLabel.toLowerCase()} work across repositories and organizations.

- Create campaign -
`; +function formatCount(value) { + return Number.isFinite(value) ? new Intl.NumberFormat("en").format(value) : "—"; } -for (const [campaignType, filename] of [["code", "campaigns.html"], ["secrets", "campaigns-secrets.html"]]) { - await writeFile(path.join(outputDirectory, filename), layout({ - title: "Campaigns", - description: "Coordinate time-bound agentic initiatives across repositories and organizations.", - content: campaignContent(campaignType), - campaignType, - })); +function deployedStandaloneWorkflows() { + return (deployedInventory.standaloneWorkflows || deployedInventory.workflows || []) + .filter((workflow) => workflow.repository !== repository); } -function deployedWorkflowContent() { - const workflows = deployedInventory.workflows || []; - const workflowByKey = new Map(workflows.map((workflow) => [`${workflow.repository}:${workflow.path}`, workflow])); - const discoveredBundles = [...(deployedInventory.bundles || [])]; - const bundleKeys = new Set(discoveredBundles.map((bundle) => `${bundle.repository}:${bundle.name}`)); - for (const bundle of bundleDefinitions) { - const key = `${repository}:${bundle.name}`; - if (bundleKeys.has(key)) continue; - const memberPaths = [bundle.workflow.replace(/\.md$/, ".lock.yml"), ...bundle.workers.map((worker) => worker.lockPath)]; - discoveredBundles.push({ - repository, - name: bundle.name, - path: bundle.workflow, - description: bundle.description, - workflows: memberPaths.map((lockPath) => { - const deployed = workflowByKey.get(`${repository}:${lockPath}`); - return { lockPath, name: deployed?.name || lockPath.split("/").at(-1).replace(/\.lock\.yml$/, ""), state: deployed?.state || "unknown" }; - }), - }); +function repositoryCoverage() { + const repositories = new Map(); + for (const workflow of deployedStandaloneWorkflows()) { + repositories.set(workflow.repository, workflow.visibility); } - const active = workflows.filter((workflow) => workflow.state === "active").length; - const disabled = workflows.filter((workflow) => workflow.state.startsWith("disabled")).length; + const visibilities = [...repositories.values()]; + return { + discovered: repositories.size, + public: visibilities.filter((visibility) => visibility === "public").length, + private: visibilities.filter((visibility) => visibility === "private").length, + organization: deployedInventory.organizationRepositories || {}, + }; +} + +function configuredDashboardScope() { + const configuredRepositories = [...new Set([ + ...(deployedInventory.allowedRepositories || []), + ...allowedRepositories, + ].map((value) => value.trim()).filter(Boolean))].sort(); + const repositoryScopeEnabled = deployedInventory.repositoryScope === "allowlist" || configuredRepositories.length > 0; + if (repositoryScopeEnabled) { + const organizations = [...new Set(configuredRepositories.map((repositoryName) => repositoryName.split("/")[0]))].sort(); + const organizationList = organizations.length > 1 + ? `${organizations.slice(0, -1).join(", ")} and ${organizations.at(-1)}` + : organizations[0] || "configured owners"; + return { + label: organizations.join(" + ") || "Repository allowlist", + description: `${formatCount(configuredRepositories.length)} configured repositories in ${organizationList}`, + title: `Repository allowlist: ${configuredRepositories.join(", ")}`, + repositories: configuredRepositories, + }; + } + const organization = deployedInventory.organization || owner; + return { + label: organization, + description: `the ${organization} organization`, + title: `Organization scope: ${organization}`, + repositories: [], + }; +} + +const dashboardScope = configuredDashboardScope(); + +await writeFile(path.join(outputDirectory, "styles.css"), stylesheet()); +await Promise.all([ + mkdir(path.join(outputDirectory, "repositories"), { recursive: true }), + mkdir(path.join(outputDirectory, "workflows"), { recursive: true }), +]); +await writeFile(path.join(outputDirectory, "index.html"), layout({ + title: "Control plane", + description: "Managed operations, execution health, and items requiring attention.", + content: deployedWorkflowContent("overview"), + activeSection: "overview", +})); +await writeFile(path.join(outputDirectory, "repositories", "index.html"), layout({ + title: "Repositories", + description: `Repository-owned workflow health and AI Credit usage across ${dashboardScope.description}.`, + content: deployedWorkflowContent("repositories"), + nested: true, + activeSection: "repositories", +})); +await writeFile(path.join(outputDirectory, "workflows", "index.html"), layout({ + title: "Workflows", + description: `Search and inspect repository-owned GitHub Agentic Workflows across ${dashboardScope.description}.`, + content: deployedWorkflowContent("workflows"), + nested: true, + activeSection: "workflows", +})); + +function deployedWorkflowContent(view) { + const workflows = deployedStandaloneWorkflows(); + const repositoryNames = new Set(workflows.map((workflow) => workflow.repository)); + const coverage = repositoryCoverage(); const health = summarizeWorkflowHealth(workflows); const healthLabel = deployedInventory.runHealth?.available - ? `${deployedInventory.runHealth.complete ? "Complete" : "Partial"} ${deployedInventory.runHealth.windowHours || 24}-hour audit-log window` - : "Organization audit log unavailable"; - const spend = contributionSpendFor(); - const repositoriesWithWorkflows = new Set(workflows.map((workflow) => workflow.repository)); - const bundleRows = discoveredBundles.sort((left, right) => left.repository.localeCompare(right.repository) || left.name.localeCompare(right.name)).map((bundle) => { - const activeMembers = bundle.workflows.filter((workflow) => workflow.state === "active").length; - const memberNames = bundle.workflows.map((workflow) => workflow.name).join(", ") || "No workflow sources declared"; - const repositoryUrl = repositoriesWithWorkflows.has(bundle.repository) - ? `repositories/${repositoryPageName(bundle.repository)}.html` - : `https://github.com/${bundle.repository}`; - return `${escapeHtml(bundle.name)}${escapeHtml(bundle.repository)}${bundle.workflows.length}${activeMembers}${escapeHtml(bundle.path)}`; - }).join("\n"); - const rows = workflows.map((workflow) => ` - ${escapeHtml(workflow.repository)} + ? `${deployedInventory.runHealth.complete ? "Complete" : "Partial"} ${deployedInventory.runHealth.windowHours || 24}-hour Actions run window` + : "Actions run data unavailable"; + const spend = contributionSpendFor(repositoryNames); + const repositories = repositorySummaries(workflows, spend); + const repositoryLinkPrefix = view === "repositories" ? "" : view === "workflows" ? "../repositories/" : "repositories/"; + const repositoryOptions = repositories.map((entry) => ``).join(""); + const rows = workflows.map((workflow) => { + const state = workflow.state === "active" ? "active" : workflow.state.startsWith("disabled") ? "disabled" : "other"; + const runState = (workflow.runHealth?.failed || 0) > 0 ? "failed" : (workflow.runHealth?.runs || 0) > 0 ? "active" : "quiet"; + const searchText = `${workflow.repository} ${workflow.name} ${workflow.path}`.toLowerCase(); + return ` + ${escapeHtml(workflow.repository)} ${escapeHtml(workflow.name)}${escapeHtml(workflow.path)} ${escapeHtml(workflow.state.replaceAll("_", " "))} ${workflow.runHealth?.runs ?? "—"} ${workflow.runHealth?.failed ?? "—"} ${escapeHtml(workflow.visibility)} - `).join("\n"); - return `
-
-
Repositories
${deployedInventory.repositoryCount || 0}

Repositories with compiled agentic workflows

-
Bundles
${discoveredBundles.length}

Organization manifests and installed control-plane bundles

-
Installed workflows
${workflows.length}

Distinct compiled workflow registrations

-
Active workflows
${active}

Registered and enabled in GitHub Actions

-
Disabled workflows
${disabled}

Registered but currently disabled

-
Runs
${deployedInventory.runHealth?.available ? health.runs : "—"}

${escapeHtml(healthLabel)}

-
Failures
${deployedInventory.runHealth?.available ? health.failed : "—"}

${escapeHtml(healthLabel)}

-
AI Credits
${spend.available ? formatAic(spend.total) : "—"}

Across ${spend.reportedRuns} reported contribution run${spend.reportedRuns === 1 ? "" : "s"}

-
-
- ${biggestSpendersContent(spend)} -
-

Organization bundles

-

Agentic workflow packages discovered from organization aw.yml manifests and this repository's installed control plane.

-
${bundleRows || ''}
BundleRepositoryWorkflowsActiveDefinition
No organization bundles were discovered.
+ `; + }).join("\n"); + const scope = overviewScopeContent(coverage, healthLabel, spend); + const controlPlane = controlPlaneStatusContent(workflows, coverage, repositories, health, healthLabel, spend); + const priorities = `
+ ${attentionContent(workflows, repositories, health, spend)} + ${operationPortfolioContent()} +
`; + const repositoryView = `${repositoryHealthContent(repositories, deployedInventory.runHealth?.available, repositoryLinkPrefix)} + ${contributionSpendContent(spend, repositoryLinkPrefix)}`; + const workflowCatalog = `
+
Inventory

Standalone AW workflows

Search repository-owned compiled workflows outside managed operation manifests.

${formatCount(workflows.length)} workflows
+
+ Browse workflow catalog +
+ + + + +
+

+
+ ${rows || ''}
RepositoryWorkflowStateRunsFailedVisibilityUpdated
No compiled AW workflows were discovered.
+
+ +
-
-

Installed workflows

-

Compiled .github/workflows/*.lock.yml workflows visible to the report token in ${escapeHtml(deployedInventory.organization || owner)}.

-
- ${rows || ''}
RepositoryWorkflowStateRunsFailedVisibilityUpdated
No compiled agentic workflows were discovered.
+ `; + if (view === "repositories") return `${repositoryView}${scope}`; + if (view === "workflows") return workflowCatalog; + return `${controlPlane}${priorities}`; +} + +function overviewScopeContent(coverage, healthLabel, spend) { + const repositoryScope = dashboardScope.repositories.length > 0 + ? `
Repository scope · ${formatCount(dashboardScope.repositories.length)} configured
    ${dashboardScope.repositories.map((repositoryName) => `
  • ${escapeHtml(repositoryName)}
  • `).join("")}
` + : `
Repository scope${escapeHtml(dashboardScope.label)}${formatCount(coverage.discovered)} discovered repositories
`; + return `
+ ${repositoryScope} +
Run window${escapeHtml(healthLabel)}
+
AIC coverage${spend.available ? `${formatCount(spend.reportedRuns)} artifacts${spend.complete ? "" : " · partial"}` : "Unavailable"}
+
`; +} + +function controlPlaneStatusContent(workflows, coverage, repositories, health, healthLabel, spend) { + const runHealthAvailable = deployedInventory.runHealth?.available; + const failureRepositories = repositories.filter((entry) => entry.health.failed > 0).length; + const disabled = workflows.filter((workflow) => workflow.state.startsWith("disabled")).length; + const active = workflows.filter((workflow) => workflow.state === "active").length; + const managedWorkflows = bundleDefinitions.reduce((total, bundle) => total + bundleCapacityWorkflows(bundle).length, 0); + const inventoryWarnings = bundleDefinitions.reduce((total, bundle) => total + (bundle.compiled ? 0 : 1) + bundle.missingWorkers.length, 0); + const coverageGap = !deployedInventory.includePrivate || !runHealthAvailable || !deployedInventory.runHealth.complete || !spend.available || !spend.complete; + const attentionSignals = Number(health.failed > 0) + Number(disabled > 0) + Number(health.pending > 0) + Number(coverageGap); + const failureRate = health.runs > 0 ? health.failed / health.runs : 0; + const otherRuns = Math.max(0, health.runs - health.successful - health.failed - health.pending); + const percent = (value) => health.runs > 0 ? `${(value / health.runs * 100).toFixed(2)}%` : "0%"; + const state = health.failed > 0 || inventoryWarnings > 0 + ? { className: "control-plane-critical", icon: "issue", label: "Attention required" } + : health.pending > 0 || coverageGap + ? { className: "control-plane-monitoring", icon: "play", label: "Monitoring" } + : { className: "control-plane-healthy", icon: "check-circle", label: "Healthy" }; + const summary = health.failed > 0 + ? `${formatCount(health.failed)} of ${formatCount(health.runs)} runs failed across ${formatCount(failureRepositories)} of ${formatCount(repositories.length)} repositories in the current window.` + : !runHealthAvailable + ? "Run telemetry is unavailable, so execution health cannot be determined." + : health.pending > 0 + ? `${formatCount(health.pending)} runs are in progress; no failures are currently observed.` + : health.runs > 0 + ? `No failures observed across ${formatCount(health.runs)} runs in the current window.` + : "No workflow runs were observed in the current window."; + const failureRateLabel = runHealthAvailable && health.runs > 0 + ? new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(failureRate) + : "—"; + const aicCoverage = spend.available ? `${formatCount(spend.reportedRuns)} AIC artifacts${spend.complete ? "" : " · partial"}` : "AIC unavailable"; + return `
+
+
+ ${octicon(state.icon)} +
Control plane · ${escapeHtml(dashboardScope.label)}

${state.label}

${escapeHtml(summary)}

+
+ ${formatCount(attentionSignals)}attention signal${attentionSignals === 1 ? "" : "s"} +
+
+
Managed operations
${formatCount(bundleDefinitions.length)}

${formatCount(managedWorkflows)} worker workflow${managedWorkflows === 1 ? "" : "s"}

+
Active workflows
${formatCount(active)}

${formatCount(disabled)} disabled · ${formatCount(coverage.discovered)} repositories

+
Runs · 24h
${runHealthAvailable ? formatCount(health.runs) : "—"}

${escapeHtml(healthLabel)}

+
Failure rate
${failureRateLabel}

${runHealthAvailable ? `${formatCount(health.failed)} failed runs` : "Telemetry unavailable"}

+
Running now
${runHealthAvailable ? formatCount(health.pending) : "—"}

Queued or in progress

+
+
+
24-hour execution health${escapeHtml(aicCoverage)}
+ +
  • Successful ${formatCount(health.successful)}
  • Failed ${formatCount(health.failed)}
  • Running ${formatCount(health.pending)}
  • Other ${formatCount(otherRuns)}
`; } +function repositorySummaries(workflows, spend) { + const spendByRepository = new Map(spend.repositories.map((entry) => [entry.repository, entry.aiCredits])); + const summaries = new Map(); + for (const workflow of workflows) { + const summary = summaries.get(workflow.repository) || { + repository: workflow.repository, + workflows: 0, + active: 0, + disabled: 0, + health: { runs: 0, successful: 0, failed: 0, cancelled: 0, skipped: 0, pending: 0, other: 0 }, + aiCredits: spendByRepository.get(workflow.repository) || 0, + }; + summary.workflows += 1; + if (workflow.state === "active") summary.active += 1; + if (workflow.state.startsWith("disabled")) summary.disabled += 1; + for (const key of Object.keys(summary.health)) summary.health[key] += workflow.runHealth?.[key] || 0; + summaries.set(workflow.repository, summary); + } + return [...summaries.values()].sort((left, right) => right.health.failed - left.health.failed || right.health.runs - left.health.runs || left.repository.localeCompare(right.repository)); +} + +function attentionContent(workflows, repositories, health, spend) { + const disabled = workflows.filter((workflow) => workflow.state.startsWith("disabled")); + const failureRepositories = repositories.filter((entry) => entry.health.failed > 0); + const dataGaps = []; + if (!deployedInventory.includePrivate) dataGaps.push("private repository discovery is off"); + if (!deployedInventory.runHealth?.available) dataGaps.push("run telemetry is unavailable"); + else if (!deployedInventory.runHealth.complete) dataGaps.push("run telemetry is partial"); + if (!spend.available) dataGaps.push("AIC telemetry is unavailable"); + else if (!spend.complete) dataGaps.push("AIC telemetry is partial"); + const items = []; + if (health.failed > 0) items.push(`
  • ${octicon("issue")}
    ${formatCount(health.failed)} failed runsAcross ${formatCount(failureRepositories.length)} repositor${failureRepositories.length === 1 ? "y" : "ies"} in the current window
    Review
  • `); + if (disabled.length > 0) items.push(`
  • ${octicon("eye")}
    ${formatCount(disabled.length)} disabled workflowsRepository-owned workflows not currently active
    Inspect
  • `); + if (health.pending > 0) items.push(`
  • ${octicon("play")}
    ${formatCount(health.pending)} runs in progressPending completion in the current run window
    Track
  • `); + if (dataGaps.length > 0) items.push(`
  • ${octicon("codescan")}
    Coverage needs context${escapeHtml(dataGaps.join("; "))}
  • `); + if (items.length === 0) items.push(`
  • ${octicon("check-circle")}
    No immediate attention itemsNo failures, disabled workflows, pending runs, or coverage gaps observed
  • `); + return `
    Act now

    Needs attention

    ${formatCount(items.length)}
      ${items.join("")}
    `; +} + +function operationPortfolioContent() { + const cards = bundleDefinitions.map((bundle) => { + const mode = configuredModeFor(bundle); + const capacity = bundleCapacityWorkflows(bundle).reduce((total, workflow) => total + workflow.maxAiCredits, 0); + const warnings = (bundle.compiled ? 0 : 1) + bundle.missingWorkers.length; + return `
    +
    ${octicon(bundle.id.includes("dependabot") ? "dependabot" : "meter")}${escapeHtml(bundle.name)}
    ${modeIndicator(mode)}
    +
    Workers
    ${formatCount(bundle.workers.length)}
    AIC allowance
    ${formatAic(capacity)}
    Inventory
    ${warnings ? `${warnings} warning${warnings === 1 ? "" : "s"}` : "Ready"}
    + +
    `; + }).join(""); + return `
    Control plane

    Managed operations

    View activity
    ${cards || '

    No managed operations discovered.

    '}
    `; +} + +function repositoryHealthContent(repositories, available, repositoryLinkPrefix = "repositories/") { + const rows = repositories.map((entry) => { + const failureRate = entry.health.runs > 0 ? entry.health.failed / entry.health.runs : null; + const status = entry.health.failed > 0 + ? 'Needs attention' + : entry.health.pending > 0 + ? 'In progress' + : entry.health.runs > 0 + ? 'No failures observed' + : entry.disabled > 0 + ? 'Disabled workflows' + : 'No recent runs'; + return `${escapeHtml(entry.repository)}${formatCount(entry.workflows)}${formatCount(entry.active)}${available ? formatCount(entry.health.runs) : "—"}
    ${available && failureRate !== null ? new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(failureRate) : "—"}${available ? `${formatCount(entry.health.failed)} failed` : "Unavailable"}
    ${formatAic(entry.aiCredits)}${status}`; + }).join(""); + return `
    +
    Repository view

    Health by repository

    Aggregated workflow health and observed usage, ordered by failures.

    ${formatCount(repositories.length)} repositories
    +
    ${rows || ''}
    RepositoryAWsActiveRunsFailure rateAICStatus
    No repositories discovered.
    +
    `; +} + function repositoryPageName(repositoryName) { return repositoryName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function summarizeWorkflowHealth(workflows) { return workflows.reduce((summary, workflow) => { - for (const key of ["runs", "successful", "failed", "cancelled", "other"]) summary[key] += workflow.runHealth?.[key] || 0; + for (const key of ["runs", "successful", "failed", "cancelled", "skipped", "pending", "other"]) summary[key] += workflow.runHealth?.[key] || 0; return summary; - }, { runs: 0, successful: 0, failed: 0, cancelled: 0, other: 0 }); + }, { runs: 0, successful: 0, failed: 0, cancelled: 0, skipped: 0, pending: 0, other: 0 }); +} + +function workflowSourceMetric(standaloneWorkflows) { + const operationWorkflows = bundleDefinitions.length + workerDefinitions.length; + const total = operationWorkflows + standaloneWorkflows.length; + const segments = [ + ["Operation AWs", operationWorkflows, "var(--accent)"], + ["Standalone AWs", standaloneWorkflows.length, "var(--muted)"], + ]; + let offset = 0; + const stops = total > 0 ? segments.filter(([, value]) => value > 0).map(([, value, color]) => { + const start = offset; + offset += value / total * 100; + return `${color} ${start.toFixed(3)}% ${offset.toFixed(3)}%`; + }).join(", ") : "var(--neutral-muted) 0 100%"; + const chartLabel = `AW composition: ${operationWorkflows} operation workflows, ${standaloneWorkflows.length} standalone workflows`; + const legend = segments.map(([label, value, color]) => `
  • ${label}${value}
  • `).join(""); + return `
    AW composition
    ${total}workflows

    Managed operation workflows versus repository-owned workflows

    `; +} + +function workflowStatusMetric(workflows) { + const active = workflows.filter((workflow) => workflow.state === "active").length; + const disabled = workflows.filter((workflow) => workflow.state.startsWith("disabled")).length; + const unknown = workflows.length - active - disabled; + const segments = [ + ["Active", active, "var(--success)"], + ["Disabled", disabled, "var(--cancelled)"], + ["Unknown", unknown, "var(--attention)"], + ]; + let offset = 0; + const stops = workflows.length > 0 ? segments.filter(([, value]) => value > 0).map(([, value, color]) => { + const start = offset; + offset += value / workflows.length * 100; + return `${color} ${start.toFixed(3)}% ${offset.toFixed(3)}%`; + }).join(", ") : "var(--neutral-muted) 0 100%"; + const chartLabel = `Workflow status: ${active} active, ${disabled} disabled, ${unknown} unknown`; + const legend = segments.map(([label, value, color]) => `
  • ${label}${value}
  • `).join(""); + return `
    Workflow status
    ${workflows.length}workflows

    Current GitHub Actions registration state

    `; +} + +function workflowHealthMetric(health, available, coverageLabel) { + const inactive = health.cancelled + health.skipped + health.other; + const segments = [ + ["Successful", health.successful, "var(--success)"], + ["Failed", health.failed, "var(--danger)"], + ["Pending", health.pending, "var(--attention)"], + ["Skipped / neutral / stale / cancelled", inactive, "var(--cancelled)"], + ]; + let offset = 0; + const stops = available && health.runs > 0 ? segments.filter(([, value]) => value > 0).map(([, value, color]) => { + const start = offset; + offset += value / health.runs * 100; + return `${color} ${start.toFixed(3)}% ${offset.toFixed(3)}%`; + }).join(", ") : "var(--neutral-muted) 0 100%"; + const chartLabel = available + ? `Run health: ${health.successful} successful, ${health.failed} failed, ${health.pending} pending, ${inactive} skipped, neutral, stale, or cancelled` + : "Run health unavailable"; + const legend = segments.map(([label, value, color]) => `
  • ${label}${available ? value : "—"}
  • `).join(""); + return `
    Run health
    ${available ? health.runs : "—"}runs

    ${escapeHtml(coverageLabel)}

    `; } function contributionSpendFor(repositoryNames) { const included = repositoryNames ? new Set(repositoryNames) : null; - const reportedRuns = collectRuns(reportRecords).filter((run) => run.aic !== null && run.repository && (!included || included.has(run.repository))); + const coverage = (aicUsage.repositories || []).filter((entry) => !included || included.has(entry.repository)); + const reportedRuns = (aicUsage.runs || []).filter((run) => run.repository && (!included || included.has(run.repository))); const totals = new Map(); for (const run of reportedRuns) totals.set(run.repository, (totals.get(run.repository) || 0) + run.aic); const repositories = [...totals].map(([repositoryName, aiCredits]) => ({ repository: repositoryName, aiCredits })) .filter((entry) => entry.aiCredits > 0) .sort((left, right) => right.aiCredits - left.aiCredits); - return { available: reportedRuns.length > 0, reportedRuns: reportedRuns.length, repositories, total: reportedRuns.reduce((total, run) => total + run.aic, 0) }; + return { + available: coverage.length > 0 && coverage.every((entry) => entry.available), + complete: coverage.length > 0 && coverage.every((entry) => entry.complete), + reportedRuns: reportedRuns.length, + repositories, + total: reportedRuns.reduce((total, run) => total + run.aic, 0), + }; } -function biggestSpendersContent(spend) { +function contributionSpendContent(spend, repositoryLinkPrefix = "repositories/") { if (!spend.available) { - return `

    Biggest spenders

    No AI Credit usage was reported by agentic workflow contributions.

    `; + return `

    AI Credit usage by AW repository

    AI Credit usage artifacts are unavailable for this reporting window.

    `; } if (spend.total <= 0) { - return `

    Biggest spenders

    Reported agentic workflow contributions consumed 0 AIC.

    `; + return `

    AI Credit usage by AW repository

    Reported AW runs consumed 0 AI Credits.

    `; } const colors = ["#4493f8", "#3fb950", "#d29922", "#f85149", "#a371f7", "#8c959f"]; const leading = spend.repositories.slice(0, 5); @@ -790,16 +1204,18 @@ function biggestSpendersContent(spend) { return `${colors[index]} ${start.toFixed(3)}% ${offset.toFixed(3)}%`; }).join(", "); const chartLabel = segments.map((entry) => `${entry.repository}: ${formatAic(entry.aiCredits)} AI Credits`).join(", "); - const legend = segments.map((entry, index) => `
  • ${entry.repository === "Other" ? "Other" : `${escapeHtml(entry.repository)}`}${formatAic(entry.aiCredits)}${new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(entry.aiCredits / spend.total)}
  • `).join("\n"); - return `

    Biggest spenders

    AI Credits reported by agentic workflow contributions, deduplicated by workflow run.

      ${legend}
    `; + const legend = segments.map((entry, index) => `
  • ${entry.repository === "Other" ? "Other" : `${escapeHtml(entry.repository)}`}${formatAic(entry.aiCredits)}${new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(entry.aiCredits / spend.total)}
  • `).join("\n"); + return `

    AI Credit usage by AW repository

    Read-only usage reported by AW runs, deduplicated by workflow run.

      ${legend}
    `; } function repositoryWorkflowContent(repositoryName, workflows) { - const active = workflows.filter((workflow) => workflow.state === "active").length; const disabled = workflows.filter((workflow) => workflow.state.startsWith("disabled")).length; const latest = workflows.map((workflow) => workflow.updatedAt).filter(Boolean).sort().at(-1); const health = summarizeWorkflowHealth(workflows); const healthAvailable = deployedInventory.runHealth?.available; + const healthLabel = healthAvailable + ? `${deployedInventory.runHealth.complete ? "Complete" : "Partial"} ${deployedInventory.runHealth.windowHours || 24}-hour Actions run window` + : "Actions run data unavailable"; const repositorySpend = contributionSpendFor([repositoryName]); const rows = workflows.map((workflow) => ` ${escapeHtml(workflow.name)}${escapeHtml(workflow.path)} @@ -808,38 +1224,47 @@ function repositoryWorkflowContent(repositoryName, workflows) { ${workflow.runHealth?.failed ?? "—"} `).join("\n"); - return `
    + return `
    -
    Installed workflows
    ${workflows.length}

    Compiled agentic workflows in this repository

    -
    Active workflows
    ${active}

    Registered and enabled in GitHub Actions

    -
    Runs
    ${healthAvailable ? health.runs : "—"}

    Agentic runs in the last ${deployedInventory.runHealth?.windowHours || 24} hours

    -
    Failures
    ${healthAvailable ? health.failed : "—"}

    ${deployedInventory.runHealth?.complete ? "Complete audit-log window" : "Partial or unavailable audit-log window"}

    -
    AI Credits
    ${repositorySpend.available ? formatAic(repositorySpend.total) : "—"}

    Across ${repositorySpend.reportedRuns} reported contribution run${repositorySpend.reportedRuns === 1 ? "" : "s"}

    +
    Standalone AW workflows
    ${workflows.length}

    Compiled workflows outside managed operations

    + ${workflowStatusMetric(workflows)} + ${workflowHealthMetric(health, healthAvailable, healthLabel)} +
    AI Credits
    ${repositorySpend.available ? formatAic(repositorySpend.total) : "—"}

    Across ${repositorySpend.reportedRuns} retained usage artifact${repositorySpend.reportedRuns === 1 ? "" : "s"}${repositorySpend.complete ? "" : "; partial coverage"}

    -

    Installed workflows

    Compiled workflows under .github/workflows/. Latest registration update: ${escapeHtml(formatDay(latest))}. ${disabled} disabled.

    View Actions${octicon("external-link")}
    +

    Standalone AW workflows

    Compiled workflows under .github/workflows/ outside managed operation manifests. Latest registration update: ${escapeHtml(formatDay(latest))}. ${disabled} disabled.

    View Actions${octicon("external-link")}
    ${rows}
    WorkflowStateRunsFailedUpdated
    `; } -await mkdir(path.join(outputDirectory, "repositories"), { recursive: true }); const deployedByRepository = new Map(); -for (const workflow of deployedInventory.workflows || []) { +for (const workflow of deployedStandaloneWorkflows()) { const workflows = deployedByRepository.get(workflow.repository) || []; workflows.push(workflow); deployedByRepository.set(workflow.repository, workflows); } for (const [repositoryName, workflows] of deployedByRepository) { - const navigation = ``; - await writeFile(path.join(outputDirectory, "repositories", `${repositoryPageName(repositoryName)}.html`), layout({ + const pageName = repositoryPageName(repositoryName); + const repositoryRecords = reportRecords.filter((record) => record.repository.toLowerCase() === repositoryName.toLowerCase()); + const navigation = (view) => ``; + await writeFile(path.join(outputDirectory, "repositories", `${pageName}.html`), layout({ title: repositoryName, - description: "Agentic workflows installed and registered in this repository.", - content: repositoryWorkflowContent(repositoryName, workflows), + description: "Durable reports produced for this repository by centrally managed operations.", + content: `${repositoryTabs(repositoryName, "reports")}${findingsListing(repositoryRecords, { showMode: true, emptyMessage: "No reports have been recorded for this repository." })}`, nested: true, - navigation, + navigation: navigation("Reports"), + activeSection: "repositories", + })); + await writeFile(path.join(outputDirectory, "repositories", `${pageName}-insights.html`), layout({ + title: repositoryName, + description: "Workflow health, registration state, and AI Credit usage for this repository.", + content: `${repositoryTabs(repositoryName, "insights")}${repositoryWorkflowContent(repositoryName, workflows)}`, + nested: true, + navigation: navigation("Insights"), + activeSection: "repositories", })); } @@ -886,7 +1311,7 @@ function valueReportContent(worker, artifact, assetName) { await mkdir(path.join(outputDirectory, "insights", "assets"), { recursive: true }); for (const bundle of bundleDefinitions) { - const navigation = ``; + const navigation = ``; const sections = []; for (const worker of bundle.workers) { const artifact = valueTimelines.get(worker.id); @@ -902,9 +1327,9 @@ for (const bundle of bundleDefinitions) { sections.push(valueReportContent(worker, valueTimelines.get(worker.id), assetName)); } await writeFile(path.join(outputDirectory, "insights", `${bundle.id}.html`), layout({ - title: `${bundle.name} insights`, + title: bundle.name, description: `Worker operational-value measurements from the ${bundle.name} value functions.`, - content: sections.join("\n"), + content: `${bundleTabs(bundle, "insights")}${sections.join("\n")}`, nested: true, navigation, activeSection: "insights", @@ -912,46 +1337,59 @@ for (const bundle of bundleDefinitions) { })); } -await mkdir(path.join(outputDirectory, "bundles"), { recursive: true }); +await mkdir(path.join(outputDirectory, "operations"), { recursive: true }); +const defaultOperationsMode = bundleDefinitions.some((bundle) => configuredModeFor(bundle) === "live") ? "live" : "review"; +for (const mode of ["review", "live"]) { + const page = layout({ + title: "Operations", + description: `${modeLabels[mode]} activity from centrally managed operations.`, + content: operationsOverviewContent(mode), + nested: true, + overviewMode: mode, + activeSection: "operations", + }); + await writeFile(path.join(outputDirectory, "operations", `${mode}.html`), page); + if (mode === defaultOperationsMode) await writeFile(path.join(outputDirectory, "operations", "index.html"), page); +} for (const bundle of bundleDefinitions) { const bundleRecords = reportRecords.filter((record) => record.bundle === bundle.id); - const navigation = ``; + const navigation = ``; const configuredMode = configuredModeFor(bundle); - const defaultMode = configuredMode; - const modeIdentities = { - staged: "Viewing staged output without repository writes", - review: "Viewing proposals routed for human review", - live: "Viewing production outputs from live operation", - }; - for (const selectedMode of ["staged", "review", "live"]) { + const defaultMode = configuredMode === "live" ? "live" : "review"; + for (const selectedMode of ["review", "live"]) { const modeRecords = bundleRecords.filter((record) => record.mode === selectedMode); - const content = `

    ${escapeHtml(modeIdentities[selectedMode])}.

    ${modeTabs(bundle, selectedMode)}${findingsListing(modeRecords)}`; + const selectedModeLabel = selectedMode === "review" ? "Review proposals" : "Live production outputs"; + const configuredModeLabel = `${configuredMode[0].toUpperCase()}${configuredMode.slice(1)}`; + const modeIdentity = selectedMode === configuredMode + ? `${selectedModeLabel}; this is the operation's configured mode.` + : `${selectedModeLabel}; the operation is currently configured for ${configuredModeLabel}.`; + const content = `${bundleTabs(bundle, "reports")}${modeTabs(bundle, selectedMode)}

    ${escapeHtml(modeIdentity)}

    ${findingsListing(modeRecords)}`; const page = layout({ - title: `${bundle.name} findings`, - description: `Durable reports produced by the ${bundle.name} control-plane bundle.`, + title: bundle.name, + description: `Durable reports produced by the ${bundle.name} operation.`, content, nested: true, navigation, configuredMode, - activeSection: "findings", + activeSection: "operations", activeBundle: bundle.id, }); - await writeFile(path.join(outputDirectory, "bundles", `${bundle.id}-${selectedMode}.html`), page); - if (selectedMode === defaultMode) await writeFile(path.join(outputDirectory, "bundles", `${bundle.id}.html`), page); + await writeFile(path.join(outputDirectory, "operations", `${bundle.id}-${selectedMode}.html`), page); + if (selectedMode === defaultMode) await writeFile(path.join(outputDirectory, "operations", `${bundle.id}.html`), page); } } -await mkdir(path.join(outputDirectory, "workflows"), { recursive: true }); for (const workflow of standaloneDefinitions) { const workflowRecords = reportRecords.filter((record) => record.bundle === workflow.id); - const navigation = ``; + const navigation = ``; const content = `

    Workflow inventory

    ${workflow.compiled ? "Source and compiled lock file are present." : "Source is present without a matching compiled lock file."}

    ${escapeHtml(workflow.sourcePath)}

    ${outcomeListing(workflowRecords)}`; await writeFile(path.join(outputDirectory, "workflows", `${workflow.id}.html`), layout({ title: workflow.name, - description: workflow.description || "Standalone agentic workflow.", + description: workflow.description || "Standalone GitHub Agentic Workflow.", content, nested: true, navigation, + activeSection: "workflows", })); } @@ -1118,17 +1556,19 @@ tbody tr:hover { background: var(--canvas-subtle); } .findings-header h2 { margin: 0; } .findings-header > div { color: var(--muted); font-size: .75rem; } .findings-header > div span { margin-left: 14px; } -.finding-columns { display: grid; grid-template-columns: minmax(298px, 1fr) 70px 145px 60px 100px; gap: 12px; padding: 7px 14px 7px 64px; border-top: 1px solid var(--border); color: var(--muted); font-size: .6875rem; font-weight: 600; } -.finding-row { min-height: 58px; display: grid; grid-template-columns: 38px minmax(248px, 1fr) 70px 145px 60px 100px; align-items: center; gap: 12px; padding: 8px 14px; border-top: 1px solid var(--border-muted); } +.finding-columns { display: grid; grid-template-columns: minmax(298px, 1fr) 70px 60px 150px; gap: 12px; padding: 7px 14px 7px 64px; border-top: 1px solid var(--border); color: var(--muted); font-size: .6875rem; font-weight: 600; } +.finding-row { min-height: 58px; display: grid; grid-template-columns: 38px minmax(248px, 1fr) 70px 60px 150px; align-items: center; gap: 12px; padding: 8px 14px; border-top: 1px solid var(--border-muted); } +.findings-with-mode .finding-columns { grid-template-columns: minmax(248px, 1fr) 70px 70px 60px 150px; } +.findings-with-mode .finding-row { grid-template-columns: 38px minmax(198px, 1fr) 70px 70px 60px 150px; } .finding-row:hover { background: var(--canvas-subtle); } .finding-icon { width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 6px; color: var(--muted); } .finding-report { min-width: 0; } .finding-report h3 { margin: 0; overflow: hidden; } .finding-report h3 a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .finding-report p { margin: 3px 0 0; overflow: hidden; color: var(--muted); font-size: .75rem; text-overflow: ellipsis; white-space: nowrap; } -.finding-workflow, .finding-row time { overflow: hidden; color: var(--muted); font-size: .75rem; text-overflow: ellipsis; white-space: nowrap; } +.finding-row time { overflow: hidden; color: var(--muted); font-size: .75rem; text-overflow: ellipsis; white-space: nowrap; } .kind, .status, .mode-badge { display: inline-flex; align-items: center; min-height: 20px; padding: 0 7px; border: 1px solid var(--border); border-radius: 2em; color: var(--muted); font-size: .6875rem; font-weight: 600; text-transform: capitalize; white-space: nowrap; } -.finding-row > .kind, .finding-row > .status { justify-self: start; } +.finding-row > .kind, .finding-row > .status, .finding-row > .mode-badge { justify-self: start; } .status-success { border-color: color-mix(in srgb, var(--success) 45%, var(--border)); background: var(--success-muted); color: var(--success); } .status-attention { border-color: color-mix(in srgb, var(--attention) 45%, var(--border)); background: var(--attention-muted); color: var(--attention); } .status-muted { background: var(--neutral-muted); } @@ -1138,7 +1578,7 @@ tbody tr:hover { background: var(--canvas-subtle); } .mode-indicator { min-height: 22px; display: inline-flex; flex: none; align-items: center; gap: 5px; padding: 1px 7px; border: 1px solid var(--border); border-radius: 2em; font-size: .6875rem; font-weight: 600; text-transform: none; white-space: nowrap; } .mode-indicator .octicon { width: 13px; height: 13px; flex-basis: 13px; } .sidebar-nav .mode-indicator { margin-left: auto; } -.mode-view-note { margin: 0 0 14px; color: var(--muted); } +.mode-view-note { margin: 12px 0 14px; color: var(--muted); } .mode-tabs { display: flex; margin: 20px 0 0; border-bottom: 1px solid var(--border); } .mode-tabs a { min-width: 130px; display: flex; flex-direction: column; gap: 1px; position: relative; padding: 10px 16px; color: var(--muted); text-decoration: none; } .mode-tabs a:hover { color: var(--fg); } @@ -1179,55 +1619,55 @@ footer { padding: 20px 24px; border-top: 1px solid var(--border); color: var(--m footer a { min-height: 24px; display: inline-flex; align-items: center; } .app-shell { min-height: 100vh; display: grid; grid-template-columns: 232px minmax(0, 1fr); } .org-sidebar { min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 24px 16px 16px; border-right: 1px solid var(--border); background: var(--canvas-subtle); } -.sidebar-brand { display: block; margin: 0 8px 10px; overflow: hidden; color: var(--fg); font-size: 1.125rem; font-weight: 600; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; } -.primary-nav, .sidebar-group nav { display: flex; flex-direction: column; gap: 2px; } -.primary-nav a, .sidebar-group a { min-height: 32px; display: flex; align-items: center; gap: 10px; position: relative; padding: 6px 8px; border-radius: 6px; color: var(--fg); font-weight: 500; text-decoration: none; } -.primary-nav a > .octicon, .sidebar-group a > .octicon { color: var(--muted); } -.primary-nav a:hover, .sidebar-group a:hover { background: var(--neutral-muted); } -.primary-nav a[aria-current="page"], .sidebar-group a[aria-current="page"] { background: var(--neutral-muted); font-weight: 600; } -.primary-nav a[aria-current="page"]::before, .sidebar-group a[aria-current="page"]::before { content: ""; width: 3px; position: absolute; top: 5px; bottom: 5px; left: -16px; border-radius: 0 4px 4px 0; background: var(--accent); } -.sidebar-group { margin-top: 12px; padding-top: 18px; border-top: 1px solid var(--border); } -.sidebar-group > p { margin: 0 8px 6px; color: var(--muted); font-size: .75rem; font-weight: 600; text-transform: uppercase; } -.findings-nav { margin-top: 12px; } -.sidebar-repository { margin-top: auto; padding: 16px 8px 0; border-top: 1px solid var(--border); color: var(--muted); font-size: .75rem; } -.sidebar-repository span, .sidebar-repository a { display: block; } -.sidebar-repository a { margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sidebar-brand { display: flex; align-items: center; gap: 6px; margin: 0 8px 10px; overflow: hidden; color: var(--fg); font-size: 1rem; font-weight: 600; text-decoration: none; white-space: nowrap; } +.sidebar-brand-mark { width: 24px; height: 24px; flex: 0 0 24px; overflow: visible; } +.sidebar-brand > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; } +.primary-nav { display: flex; flex-direction: column; gap: 2px; } +.primary-nav a, .nav-parent { min-height: 32px; display: flex; align-items: center; gap: 10px; position: relative; padding: 6px 8px; border-radius: 6px; color: var(--fg); font-weight: 500; text-decoration: none; } +.primary-nav :is(a, .nav-parent) > .octicon { color: var(--muted); } +.primary-nav a:hover { background: var(--neutral-muted); } +.primary-nav a[aria-current="page"] { background: var(--neutral-muted); font-weight: 600; } +.primary-nav a[aria-current="page"]::before { content: ""; width: 3px; position: absolute; top: 5px; bottom: 5px; left: -16px; border-radius: 0 4px 4px 0; background: var(--accent); } +.nav-family { margin-top: 2px; } +.nav-parent { font-weight: 600; } +.nav-children { display: flex; flex-direction: column; gap: 2px; margin-left: 18px; padding-left: 7px; border-left: 1px solid var(--border); } +.nav-children a[aria-current="page"]::before { left: -8px; } .app-main { min-width: 0; display: flex; flex-direction: column; } .app-main > nav { border-bottom: 1px solid var(--border); } -.app-main > nav .shell { display: flex; gap: 8px; max-width: 1280px; margin: auto; padding: 10px 24px; } +.app-main > nav .shell { display: flex; align-items: center; gap: 8px; max-width: 1280px; margin: auto; padding: 10px 24px; } .app-main > nav .shell > a { min-height: 24px; display: inline-flex; align-items: center; } -.app-main > nav .shell > * + *::before { content: "/"; margin-right: 8px; color: var(--muted); } +.app-main > nav .shell > * + *:not(.report-actions)::before { content: "/"; margin-right: 8px; color: var(--muted); } +.report-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; } +.app-main > nav .freshness { max-width: none; flex: none; white-space: nowrap; } +.repository-link { width: 28px; height: 28px; display: grid; flex: 0 0 28px; place-items: center; border-radius: 6px; color: var(--muted); text-decoration: none; transition: background-color 120ms ease, color 120ms ease; } +.repository-link:hover { background: var(--neutral-muted); color: var(--fg); } +.repository-link .octicon { width: 18px; height: 18px; } .overview-header { min-height: 88px; display: flex; align-items: flex-start; justify-content: space-between; gap: 32px; padding: 18px 0 14px; } .overview-header h1 { margin: 0; font-size: 1.5rem; line-height: 1.25; } .overview-header .lede { margin: 3px 0 0; font-size: .875rem; } -.overview-header .freshness { flex: none; margin: 7px 0 0; color: var(--muted); font-size: .75rem; } .toolbar { display: flex; align-items: center; gap: 8px; } -.filter-control { min-width: 240px; min-height: 30px; display: flex; flex: 1; align-items: stretch; overflow: hidden; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); font-size: .75rem; } +.filter-control { min-width: 240px; min-height: 30px; display: flex; flex: 1; align-items: stretch; position: relative; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); font-size: .75rem; } +.filter-control > summary { min-width: 0; min-height: 28px; display: flex; flex: 1; align-items: stretch; overflow: hidden; border-radius: 5px; cursor: pointer; list-style: none; } +.filter-control > summary::-webkit-details-marker { display: none; } +.filter-control[open] > summary { box-shadow: inset 0 0 0 1px var(--focus); } .scope-label, .scope-period, .export-control, .search-control { display: inline-flex; align-items: center; gap: 7px; padding: 4px 12px; } .scope-label { border-right: 1px solid var(--border); } .count-badge { min-width: 20px; padding: 0 6px; border-radius: 2em; background: var(--neutral-muted); font-size: .6875rem; text-align: center; } .filter-control code { min-width: 0; flex: 1; padding: 5px 12px; overflow: hidden; background: transparent; color: var(--accent); text-overflow: ellipsis; white-space: nowrap; } .search-control { padding-inline: 9px; border-left: 1px solid var(--border); color: var(--muted); } +.overview-toolbar { justify-content: flex-end; } .scope-period, .export-control { min-height: 30px; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas-subtle); color: var(--fg); font-size: .75rem; font-weight: 600; text-decoration: none; white-space: nowrap; } .scope-note { margin: 8px 0 15px; color: var(--muted); font-size: .75rem; } .scope-note a { color: inherit; } -.report-tabs { display: flex; margin: 0 0 22px; border-bottom: 1px solid var(--border); } -.report-tabs a { position: relative; margin-bottom: -1px; padding: 8px 14px; border: 1px solid transparent; color: var(--muted); font-weight: 600; text-decoration: none; } -.report-tabs a:hover { color: var(--fg); } -.report-tabs a[aria-current="page"] { border-color: var(--border) var(--border) var(--canvas); border-radius: 6px 6px 0 0; background: var(--canvas); color: var(--fg); } -.campaign-tabs { display: flex; gap: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border); } -.campaign-tabs a { display: inline-flex; align-items: center; gap: 8px; position: relative; padding: 10px 14px 12px; color: var(--fg); font-weight: 600; text-decoration: none; } -.campaign-tabs a > .octicon { color: var(--muted); } -.campaign-tabs a:hover { background: var(--canvas-subtle); } -.campaign-tabs a[aria-current="page"]::after { content: ""; height: 2px; position: absolute; right: 8px; bottom: -1px; left: 8px; background: #f78166; } -.campaign-tabs strong { min-width: 20px; padding: 0 6px; border-radius: 2em; background: var(--neutral-muted); color: var(--muted); font-size: .6875rem; text-align: center; } -.campaign-empty { min-height: 330px; display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 0 !important; padding: 40px 24px !important; border: 1px solid var(--border) !important; border-radius: 6px !important; text-align: center; } -.campaign-empty .campaign-empty-icon { width: 32px; height: 32px; flex-basis: 32px; color: var(--muted); } -.campaign-empty h2 { margin: 18px 0 6px; font-size: 1.25rem; } -.campaign-empty p { max-width: 620px; margin: 0; color: var(--muted); } -.campaign-create { display: inline-flex; align-items: center; min-height: 32px; margin-top: 22px; padding: 5px 16px; border: 1px solid #2ea043; border-radius: 6px; background: #238636; color: #fff; font-size: .875rem; font-weight: 600; text-decoration: none; } -.campaign-create:hover { background: #2ea043; text-decoration: none; } -.campaign-create:active { background: #238636; } +.scope-boundary { margin: 0 0 16px; padding: 12px 14px; border-left: 3px solid var(--accent); background: var(--canvas-subtle); } +.scope-boundary strong { display: block; margin-bottom: 2px; } +.scope-boundary p { margin: 0; color: var(--muted); } +.bundle-tabs { display: flex; gap: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border); } +.bundle-tabs a { display: inline-flex; align-items: center; gap: 8px; position: relative; padding: 10px 14px 12px; color: var(--fg); font-weight: 600; text-decoration: none; } +.bundle-tabs a > .octicon { color: var(--muted); } +.bundle-tabs a:hover { background: var(--canvas-subtle); } +.bundle-tabs a[aria-current="page"]::after { content: ""; height: 2px; position: absolute; right: 8px; bottom: -1px; left: 8px; background: #f78166; } +.bundle-tabs { margin-bottom: 20px; } .report-body { padding-top: 0; } .report-body > section, .report-body > section:last-child { margin: 0 0 24px; padding: 0; overflow: visible; border: 0; border-radius: 0; background: transparent; } .value-report { overflow: hidden !important; border: 1px solid var(--border) !important; border-radius: 6px !important; background: var(--canvas) !important; } @@ -1261,14 +1701,120 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .value-empty p { max-width: 620px; margin: 0; color: var(--muted); } .deployed-summary { margin-top: 8px !important; } .deployed-summary .metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.scope-kicker { color: var(--muted); font-size: .75rem; font-weight: 600; letter-spacing: 0; text-transform: uppercase; } +.scope-context { display: grid; grid-template-columns: minmax(0, 2.5fr) minmax(220px, 1.3fr) minmax(180px, 1fr); margin: 0 0 24px !important; overflow: hidden !important; border: 1px solid var(--border) !important; border-radius: 6px !important; background: var(--canvas-subtle) !important; } +.scope-context > div { min-width: 0; padding: 10px 14px; border-left: 1px solid var(--border); } +.scope-context > div:first-child { border-left: 0; } +.scope-context span, .scope-context strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.scope-context span { color: var(--muted); font-size: .75rem; font-weight: 600; text-transform: uppercase; } +.scope-context strong { margin-top: 2px; font-size: .8125rem; } +.scope-repository-boundary small { display: block; margin-top: 2px; color: var(--muted); font-size: .6875rem; } +.scope-repository-set { display: flex; flex-wrap: wrap; row-gap: 2px; margin: 4px 0 0; padding: 0; list-style: none; } +.scope-repository-set li { display: inline-flex; align-items: center; } +.scope-repository-set li:not(:last-child)::after { content: "·"; margin: 0 7px; color: var(--muted); } +.scope-repository-set code { display: block; padding: 0; background: transparent; color: var(--fg); font-size: .75rem; white-space: nowrap; } +.report-body > .control-plane-status { margin: 0 0 16px; padding: 0; overflow: visible; border: 0; background: transparent; } +.control-plane-status > header { min-height: 92px; display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 14px 16px; border: 1px solid var(--border); border-left-width: 4px; border-radius: 6px 6px 0 0; background: var(--canvas-subtle); } +.control-plane-critical > header { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 7%, var(--canvas)); } +.control-plane-monitoring > header { border-left-color: var(--attention); background: color-mix(in srgb, var(--attention) 7%, var(--canvas)); } +.control-plane-healthy > header { border-left-color: var(--success); background: color-mix(in srgb, var(--success) 7%, var(--canvas)); } +.control-plane-heading { min-width: 0; display: flex; align-items: center; gap: 12px; } +.control-plane-state-icon { width: 36px; height: 36px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--canvas); box-shadow: 0 0 0 1px var(--border); } +.control-plane-state-icon .octicon { width: 18px; height: 18px; } +.control-plane-critical .control-plane-state-icon { color: var(--danger); } +.control-plane-monitoring .control-plane-state-icon { color: var(--attention); } +.control-plane-healthy .control-plane-state-icon { color: var(--success); } +.control-plane-heading h2 { margin: 1px 0 2px; font-size: 1.25rem; } +.control-plane-heading p { max-width: 720px; margin: 0; color: var(--muted); font-size: .8125rem; } +.attention-link { min-width: 116px; flex: none; display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 0 7px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); color: var(--fg); text-decoration: none; transition: background-color 120ms ease, border-color 120ms ease; } +.attention-link:hover { border-color: var(--muted); } +.attention-link strong { grid-row: span 2; color: var(--danger); font-size: 1.5rem; font-variant-numeric: tabular-nums; } +.attention-link span { color: var(--muted); font-size: .75rem; line-height: 1.2; } +.control-plane-vitals { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1px; margin: 0; padding: 0 1px 1px; overflow: hidden; border-right: 1px solid var(--border); border-left: 1px solid var(--border); background: var(--border); } +.control-plane-vitals > div { min-width: 0; padding: 10px 13px; background: var(--canvas); } +.control-plane-vitals dt { color: var(--muted); font-size: .75rem; font-weight: 600; text-transform: uppercase; } +.control-plane-vitals dd { margin: 1px 0 0; font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; } +.control-plane-vitals p { min-height: 2.6em; margin: 0; color: var(--muted); font-size: .75rem; line-height: 1.3; } +.control-plane-vitals .vital-failures dd { color: var(--danger); } +.control-plane-vitals .vital-running dd { color: var(--attention); } +.execution-health { padding: 9px 13px 11px; border: 1px solid var(--border); border-top: 0; border-radius: 0 0 6px 6px; background: var(--canvas); } +.execution-health-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: .75rem; } +.execution-health-heading span { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } +.execution-track { height: 7px; display: flex; margin-top: 7px; overflow: hidden; border-radius: 4px; background: var(--neutral-muted); } +.execution-track span { height: 100%; display: block; } +.execution-success { background: var(--success); } +.execution-failed { background: var(--danger); } +.execution-running { min-width: 2px; background: var(--attention); } +.execution-other { background: var(--muted); } +.execution-legend { display: flex; flex-wrap: wrap; gap: 5px 16px; margin: 7px 0 0; padding: 0; color: var(--muted); font-size: .75rem; list-style: none; } +.execution-legend li { display: flex; align-items: center; gap: 5px; } +.execution-legend li > span { width: 7px; height: 7px; border-radius: 2px; } +.execution-legend strong { color: var(--fg); font-variant-numeric: tabular-nums; } +.legend-success { background: var(--success); } +.legend-failed { background: var(--danger); } +.legend-running { background: var(--attention); } +.legend-other { background: var(--muted); } +.overview-priority-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(320px, .85fr); align-items: stretch; gap: 16px; margin-bottom: 24px; } +.attention-panel, .operation-portfolio { min-width: 0; height: 100%; overflow: hidden; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); } +.attention-panel > header, .operation-portfolio > header { min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 14px; border-bottom: 1px solid var(--border); background: var(--canvas-subtle); } +.attention-panel h2, .operation-portfolio h2 { margin: 1px 0 0; font-size: 1rem; } +.attention-panel > header > strong { min-width: 24px; padding: 1px 7px; border-radius: 2em; background: var(--neutral-muted); font-size: .75rem; text-align: center; } +.attention-panel ul { margin: 0; padding: 0; list-style: none; } +.attention-panel li { min-height: 62px; display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border-muted); } +.attention-panel li:last-child { border-bottom: 0; } +.attention-panel li > .octicon { color: var(--attention); } +.attention-panel li.attention-critical > .octicon { color: var(--danger); } +.attention-panel li div { min-width: 0; } +.attention-panel li strong, .attention-panel li span { display: block; } +.attention-panel li span { margin-top: 1px; color: var(--muted); font-size: .75rem; } +.attention-panel li > a { font-size: .75rem; font-weight: 600; } +.operation-portfolio > header > a { font-size: .75rem; } +.operation-card-list { padding: 0 14px; } +.operation-card { padding: 13px 0; border-bottom: 1px solid var(--border-muted); } +.operation-card:last-child { border-bottom: 0; } +.operation-card > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.operation-card > header > div { min-width: 0; display: flex; align-items: center; gap: 8px; } +.operation-card > header a { overflow: hidden; color: var(--fg); font-weight: 600; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; } +.operation-card > header a:hover { text-decoration: underline; } +.operation-card dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 12px 0; } +.operation-card dl div { min-width: 0; padding-right: 10px; } +.operation-card dt { color: var(--muted); font-size: .75rem; } +.operation-card dd { margin: 2px 0 0; overflow: hidden; font-size: .75rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.operation-card footer { display: flex; gap: 14px; padding: 0; border: 0; font-size: .75rem; } +.text-attention { color: var(--attention); } +.text-success { color: var(--success); } +.status-danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); background: color-mix(in srgb, var(--danger) 12%, var(--canvas)); color: var(--danger); } +.repository-health { scroll-margin-top: 16px; } +.repository-health table { min-width: 850px; } +.repository-health td { white-space: nowrap; } +.failure-rate strong, .failure-rate span { display: block; } +.failure-rate span { color: var(--muted); font-size: .6875rem; } +.section-heading > strong, .section-heading > span { flex: none; color: var(--muted); font-size: .75rem; } +.catalog-disclosure { overflow: hidden; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); } +.catalog-disclosure > summary { min-height: 46px; display: flex; align-items: center; padding: 10px 14px; background: var(--canvas-subtle); font-weight: 600; cursor: pointer; transition: background-color 120ms ease; } +.catalog-disclosure > summary:hover { background: var(--neutral-muted); } +.catalog-disclosure[open] > summary { border-bottom: 1px solid var(--border); } +.catalog-toolbar { display: grid; grid-template-columns: minmax(240px, 1.5fr) repeat(3, minmax(140px, 1fr)); gap: 10px; padding: 14px; } +.catalog-toolbar label { min-width: 0; } +.catalog-toolbar label > span { display: block; margin-bottom: 4px; color: var(--muted); font-size: .6875rem; font-weight: 600; } +.catalog-toolbar :is(input, select) { width: 100%; min-height: 34px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); color: var(--fg); font: inherit; } +.catalog-toolbar :is(input, select):focus-visible { outline: 2px solid var(--focus); outline-offset: -1px; } +.catalog-result { margin: 0; padding: 0 14px 10px; color: var(--muted); font-size: .75rem; } +.catalog-disclosure .table-region { border-right: 0; border-left: 0; border-radius: 0; } +.catalog-more { min-height: 34px; margin: 12px 14px; padding: 5px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas-subtle); color: var(--fg); font: inherit; font-size: .75rem; font-weight: 600; cursor: pointer; } +.catalog-more:hover { background: var(--neutral-muted); } +[hidden] { display: none !important; } .overview-section-heading { margin: 32px 0 12px; padding-top: 24px; border-top: 1px solid var(--border); } .overview-section-heading h2 { margin: 0 0 3px; font-size: 1.25rem; } .overview-section-heading p { margin: 0; color: var(--muted); } +.snapshot-heading { margin-bottom: 12px; } +.snapshot-heading h2 { margin: 0 0 3px; font-size: 1.25rem; } +.snapshot-heading p { margin: 0; color: var(--muted); } .deployed-workflows > h2 { margin-bottom: 3px; font-size: 1.25rem; } .deployed-workflows > p { margin: 0 0 12px; color: var(--muted); } -.organization-bundles > h2 { margin-bottom: 3px; font-size: 1.25rem; } -.organization-bundles > p { margin: 0 0 12px; color: var(--muted); } -.organization-bundles td:nth-child(3), .organization-bundles td:nth-child(4) { width: 90px; } +.organization-operations > h2 { margin-bottom: 3px; font-size: 1.25rem; } +.organization-operations > p { margin: 0 0 12px; color: var(--muted); } +.organization-operations td:nth-child(3), .organization-operations td:nth-child(4) { width: 90px; } .deployed-workflows td:nth-child(2) a, .deployed-workflows td:nth-child(2) code { display: block; } .deployed-workflows td:nth-child(2) code { width: fit-content; max-width: 420px; margin-top: 3px; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } .deployed-workflows-table { table-layout: fixed; } @@ -1298,6 +1844,7 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .spend-chart li i { width: 9px; height: 9px; border-radius: 2px; } .spend-chart li strong, .spend-chart li small { font-variant-numeric: tabular-nums; text-align: right; } .spend-chart li small { color: var(--muted); } +.spend-segment em { display: block; margin-top: 1px; color: var(--muted); font-size: .6875rem; font-style: normal; font-weight: 400; } .section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 12px; } .section-heading h2 { margin-bottom: 3px; font-size: 1.25rem; } .section-heading p { margin: 0; color: var(--muted); } @@ -1351,6 +1898,33 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .metrics dt { color: var(--fg); font-size: .875rem; font-weight: 600; text-transform: none; } .metrics dd { margin: 4px 0 0; font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; } .metrics p { margin: 3px 0 0; color: var(--muted); font-size: .75rem; } +.metrics :is(.health-metric, .workflow-status-metric, .workflow-source-metric) { min-height: 186px; grid-column: span 2; } +:is(.health-metric, .workflow-status-metric, .workflow-source-metric) dd { display: flex; align-items: center; gap: 12px; } +:is(.health-pie, .status-pie, .source-pie) { width: 64px; height: 64px; flex: 0 0 64px; border-radius: 50%; } +:is(.health-total, .status-total, .source-total) strong, :is(.health-total, .status-total, .source-total) small { display: block; } +:is(.health-total, .status-total, .source-total) strong { font-size: 1.5rem; } +:is(.health-total, .status-total, .source-total) small { color: var(--muted); font-size: .6875rem; font-weight: 500; text-transform: uppercase; } +:is(.health-metric, .workflow-status-metric, .workflow-source-metric) ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 14px; margin: 10px 0 0; padding: 0; list-style: none; } +:is(.health-metric, .workflow-status-metric, .workflow-source-metric) li { min-width: 0; display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: center; gap: 6px; color: var(--muted); font-size: .6875rem; } +:is(.health-metric, .workflow-status-metric, .workflow-source-metric) li i { width: 8px; height: 8px; border-radius: 50%; } +:is(.health-metric, .workflow-status-metric, .workflow-source-metric) li strong { color: var(--fg); font-variant-numeric: tabular-nums; } +.bundle-utilization { padding-top: 20px; } +.bundle-utilization-heading { margin-bottom: 10px; } +.bundle-utilization-heading h2 { margin-bottom: 2px; font-size: 1.25rem; } +.bundle-utilization-heading p { margin: 0; color: var(--muted); } +.bundle-utilization-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } +.bundle-utilization-item { min-width: 0; padding: 14px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--canvas); } +.bundle-utilization-item header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; } +.bundle-utilization-item header a { color: var(--fg); font-weight: 600; text-decoration: none; } +.bundle-utilization-item header a:hover { text-decoration: underline; } +.bundle-utilization-item header strong { font-size: 1.25rem; font-variant-numeric: tabular-nums; } +.utilization-track { height: 8px; margin: 12px 0 8px; overflow: hidden; border-radius: 4px; background: var(--canvas-subtle); box-shadow: inset 0 0 0 1px var(--border); } +.utilization-track span { display: block; height: 100%; border-radius: inherit; background: var(--success); } +.utilization-medium .utilization-track span { background: var(--attention); } +.utilization-high .utilization-track span { background: var(--danger); } +.utilization-empty .utilization-track span { background: var(--muted); } +.bundle-utilization-item p { min-height: 18px; margin: 0; color: var(--muted); font-size: .75rem; } +.bundle-utilization-item small { display: block; margin-top: 4px; color: var(--muted); font-size: .6875rem; } .impact-analysis > h2 { margin-bottom: 2px; font-size: 1.25rem; } .impact-analysis > p { margin: 0 0 10px; color: var(--muted); } .impact-tabs { display: flex; border-bottom: 1px solid var(--border); } @@ -1365,6 +1939,13 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } .repository-workflow-summary .metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } .spend-panel { grid-template-columns: 1fr; } + .scope-context { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .scope-repository-boundary { grid-column: 1 / -1; } + .scope-context > div:nth-child(2) { border-top: 1px solid var(--border); border-left: 0; } + .scope-context > div:nth-child(3) { border-top: 1px solid var(--border); } + .control-plane-vitals { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .overview-priority-grid { grid-template-columns: 1fr; } + .catalog-toolbar { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 700px) { .app-shell { display: block; } @@ -1372,7 +1953,7 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .sidebar-brand { margin-bottom: 8px; font-size: 1rem; } .primary-nav { width: 100%; flex-direction: row; overflow-x: auto; } .primary-nav a { min-height: 44px; flex: none; } - .sidebar-group, .sidebar-repository { display: none; } + .nav-family, .nav-children { display: contents; } .overview-header { min-height: 0; padding: 24px 0 20px; } .toolbar { align-items: stretch; flex-wrap: wrap; } .filter-control { flex-basis: 100%; } @@ -1382,7 +1963,26 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .deployed-summary .metrics, .repository-workflow-summary .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } .spend-panel, .spend-chart { grid-template-columns: 1fr; } .spend-donut { margin: auto; } - .app-main > nav .shell { padding-inline: 16px; } + .scope-context, .overview-priority-grid, .catalog-toolbar { grid-template-columns: 1fr; } + .scope-context > div { border-top: 1px solid var(--border); border-left: 0; } + .scope-context > div:first-child { border-top: 0; } + .control-plane-status > header { align-items: stretch; flex-direction: column; gap: 8px; padding: 12px; } + .control-plane-heading { align-items: flex-start; } + .control-plane-heading .scope-kicker { display: none; } + .control-plane-heading p { font-size: .75rem; } + .attention-link { width: auto; min-width: 0; align-self: flex-start; display: flex; padding: 4px 8px; } + .attention-link strong { margin-right: 2px; font-size: 1.125rem; } + .control-plane-vitals { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .control-plane-vitals > div { padding: 8px 10px; } + .control-plane-vitals > div:last-child { grid-column: span 2; } + .control-plane-vitals p { min-height: 0; white-space: normal; } + .execution-health-heading { gap: 8px; } + .execution-legend { display: none; } + .attention-panel li { grid-template-columns: 18px minmax(0, 1fr); } + .attention-panel li > a { grid-column: 2; } + .catalog-toolbar :is(input, select) { min-height: 44px; } + .app-main > nav .shell { flex-wrap: wrap; padding-inline: 16px; } + .report-actions { margin-left: auto; } .site-header { height: 56px; } .header-inner { padding: 0 16px; } .repo-nav { height: 44px; } @@ -1406,7 +2006,7 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .finding-columns { display: none; } .finding-row { grid-template-columns: 38px minmax(0, 1fr) auto; gap: 10px; } .finding-row > .status { grid-column: 3; grid-row: 1; } - .finding-workflow, .finding-row > .kind, .finding-row > time { display: none; } + .finding-row > .mode-badge, .finding-row > .kind, .finding-row > time { display: none; } .value-report > header { flex-direction: column; gap: 8px; } .value-score { text-align: left; } .value-details { grid-template-columns: 1fr; } @@ -1414,8 +2014,8 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } .discussion-sidebar { display: flex; gap: 4px; overflow-x: auto; } .discussion-sidebar h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .discussion-sidebar > div { min-width: max-content; display: flex; } - .mode-tabs { overflow-x: auto; overflow-y: hidden; } - .mode-tabs a { min-width: 120px; padding-inline: 12px; } + .mode-tabs { overflow: hidden; } + .mode-tabs a { min-width: 0; flex: 1 1 0; padding-inline: 10px; } .outcome-view { grid-template-columns: 1fr; } .outcome-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; } .control-content > nav .shell { padding-inline: 16px; } @@ -1423,7 +2023,8 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } } @media (max-width: 420px) { .overview-header { display: block; } - .overview-header .freshness { margin-top: 12px; } + .control-plane-heading { gap: 9px; } + .control-plane-state-icon { width: 32px; height: 32px; } .metrics { grid-template-columns: 1fr; } .metrics div, .metrics div:nth-child(2) { border: 1px solid var(--border); } .trend-chart svg { height: 170px; } @@ -1471,7 +2072,7 @@ footer a { min-height: 24px; display: inline-flex; align-items: center; } } } @media print { - .site-header, .repo-nav, .control-sidebar, .control-content > nav, .org-sidebar, .app-main > nav, .skip-link, .toolbar, .report-tabs { display: none; } + .site-header, .repo-nav, .control-sidebar, .control-content > nav, .org-sidebar, .app-main > nav, .skip-link, .toolbar { display: none; } .control-layout { display: block; } main { width: 100%; padding: 0; } a { color: inherit; text-decoration: underline; } @@ -1488,4 +2089,8 @@ function legacyStylesheet() { @media print{.skip-link,nav{display:none}a{color:inherit;text-decoration:underline}.shell{width:100%}.record{break-inside:avoid}}`; } -console.log(`Built ${records.length} safe-output records across ${bundleDefinitions.length} bundles in ${outputDirectory}`); \ No newline at end of file +console.log(`Built ${records.length} safe-output records across ${bundleDefinitions.length} operations in ${outputDirectory}`); +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/.github/skills/github-pages-report/SKILL.md b/.github/skills/github-pages-report/SKILL.md deleted file mode 100644 index ed4cf03..0000000 --- a/.github/skills/github-pages-report/SKILL.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: github-pages-report -description: "Design, generate, or review polished reports published with GitHub Pages. Use for HTML, CSS, charts, dashboards, or other Pages reports derived from agentic operation outputs, including when a built-in issue report moves to Pages. Applies conventional Actions publishing, GitHub/Primer visual conventions, responsive report structure, and WCAG 2.2 AA accessibility basics." -argument-hint: "Describe the report, its data, and its GitHub Pages publishing path" ---- - -# Build a GitHub Pages Report - -Produce a durable, accessible static report that feels at home on GitHub. Use this skill for every report whose primary rendered destination is GitHub Pages, whether the report belongs to a new operation or replaces a built-in issue report. - -## Procedure - -1. Identify the report's audience, primary decisions, update cadence, data sensitivity, trusted source data, and Pages base path. Do not publish private or sensitive operational data to a public Pages site. -2. Inspect an existing Pages report in the same repository and reuse its shell, tokens, components, and build process when present. Otherwise follow the contract below. -3. Put the decision summary and report freshness in the first viewport. Order the remaining content from actionable findings to supporting detail and methodology. -4. Generate semantic HTML first, then add restrained presentation and optional progressive enhancement. The report must remain understandable when JavaScript or charts fail. -5. Escape all repository-derived and model-generated values before inserting them into HTML. Never interpolate untrusted content into `innerHTML`, inline scripts, inline styles, or URL attributes without context-appropriate sanitization and protocol validation. -6. Test the built output at the repository's actual GitHub Pages project path, not only at `/`. Use relative links or a configured base URL so assets and navigation work at `//`. -7. Complete the accessibility, responsive, and publishing checks before treating the report as ready. - -## Report Contract - -Every report must include: - -- a descriptive document `` and one visible `<h1>` -- a skip link and semantic `header`, `nav` when needed, `main`, and `footer` landmarks -- report scope, generated-at timestamp with timezone, data window, and source or methodology links -- a concise outcome summary before detailed findings -- explicit empty, loading, partial-data, stale-data, and error states when those states can occur -- stable deep links for major sections and visible focus when a heading anchor is targeted -- a provenance note linking to the generating workflow run or source revision when available -- explicit `review` or `live` provenance for every published outcome; never infer production identity from status, repository location, or visual treatment - -Prefer this page order: - -1. report identity, scope, and freshness -2. outcome summary and key metrics -3. prioritized findings or recommendations -4. trends and detailed evidence -5. methodology, caveats, and provenance - -Do not turn each section into a floating card. Use full-width sections, compact metric groups, and cards only for repeated findings or genuinely bounded items. Keep key decisions visible; use disclosure widgets only for long evidence and raw detail. - -## GitHub Visual Style - -- Treat [Primer Product UI](https://primer.style/product/) as the normative design reference for the packaged control-plane report. Follow its foundations, primitives, component anatomy, interaction states, responsive behavior, and accessibility guidance even when implementing them locally without Primer packages. -- Prefer the repository's existing Primer dependencies. For a new build pipeline, use supported `@primer/css` primitives and Octicons rather than recreating GitHub components. Do not add a runtime CDN dependency solely for styling. -- Without Primer, define a small local token layer modeled on Primer semantics: canvas, inset canvas, foreground, muted foreground, border, accent, success, attention, danger, and focus colors. Provide light and dark values with `prefers-color-scheme`. -- Express typography with a semantic `rem` scale and unitless line heights so browser font-size preferences propagate through the interface. Use functional color variables rather than raw base colors in component rules. -- Use GitHub's platform-appropriate system font stack for interface text and a monospace stack for identifiers, code, and tabular numeric data. -- Keep content dense and scannable: a constrained reading width for prose, wider responsive regions for tables and charts, 6px or smaller radii, subtle borders, and little or no decorative shadow. -- Use GitHub-like status language and color semantics consistently. Pair every color with text, an icon, a pattern, or another non-color cue. -- Use Octicons when an icon improves scanning. Give icon-only controls an accessible name and tooltip; mark decorative SVGs `aria-hidden="true"` and `focusable="false"`. -- Avoid decorative gradients, oversized marketing typography, glass effects, and illustration-first layouts. This is an operational report, not a landing page. - -## Accessibility Baseline - -Meet WCAG 2.2 AA for the report's supported states: - -- Use logical heading order, native controls, meaningful link text, and DOM order that matches visual order. -- Ensure all controls and disclosures work with a keyboard. Never use a clickable `div` or hover as the only way to reveal information. -- Show a clear `:focus-visible` indicator with at least 3:1 contrast against adjacent colors. Do not hide focused content behind sticky headers. -- Maintain at least 4.5:1 contrast for normal text, 3:1 for large text and meaningful graphics, and 3:1 for control boundaries and states. -- Do not rely on color, position, shape, or animation alone to communicate meaning. -- Respect `prefers-reduced-motion`; avoid auto-playing or nonessential animation and flashing content. -- Respect `prefers-color-scheme`, `prefers-contrast`, and `forced-colors`; preserve boundaries, focus, status meaning, and chart distinctions in each supported mode. -- Give data tables a `<caption>`, header cells with the correct `scope`, and a simple structure. Provide an accessible stacked alternative on narrow screens rather than converting headers into ambiguous data. -- Give each chart a nearby title, takeaway, units, legend, and text summary. Provide the underlying values as a table or download. Do not use canvas-only information. -- Use `aria-live` only for asynchronous status changes. Do not add ARIA where native HTML already supplies the correct semantics. -- Keep browser zoom and text resizing usable through 200%, with no clipped controls, overlapping text, or loss of information. - -## Responsive and Data Design - -- Start with a single-column document flow. Add columns only where comparison benefits, and collapse them before content becomes cramped. -- Bound report width while allowing tables and visualizations to use available space. Let wide data tables scroll inside a labeled region; never make the whole page scroll horizontally. -- Use tabular numerals and consistent precision for comparable metrics. Put units in headers or labels, not repeatedly in every cell. -- State denominators, time ranges, and timezone. Distinguish zero from unavailable, not applicable, withheld, and failed collection. -- Keep metric labels adjacent to values and trends. Never use unexplained percentages, unlabeled icon badges, or color-only sparklines. -- Preserve useful print output with legible colors, expanded essential details, visible link destinations when practical, and no clipped tables. - -## Publishing Constraints - -- Emit deterministic static assets and pin build dependencies. Do not require client-side GitHub API calls when data can be generated during the workflow. -- Keep the packaged control-plane report dependency-free. Use only Node.js built-in modules and web-platform APIs; do not add npm packages, package-manager install steps, browser JavaScript, external stylesheets, web fonts, CDN assets, or runtime network requests from the generated pages. Generate HTML, CSS, SVG charts, and the Octicon sprite locally during the build. -- Keep HTML, CSS, JavaScript, and data files separate unless the existing report pipeline intentionally produces a single self-contained artifact. -- Use hashed assets or another cache-busting strategy for generated releases. Do not cache mutable report data indefinitely. -- Include a custom `404.html` only when the site has meaningful navigation recovery. Do not use SPA routing for a static report without a concrete need. -- Publish with conventional GitHub Actions workflows that deterministically rebuild from trusted, durable source data. The control plane selects the review or production destination; the publishing workflow does not infer or promote modes. -- Minimize permissions and prefer GitHub's supported Pages artifact/deploy actions with separate build and deploy jobs. The build job needs `contents: read` and `pages: write` for `actions/configure-pages`; the deploy job independently needs `pages: write` and `id-token: write` and should use the protected `github-pages` environment. -- Do not pass generated HTML, shell commands, arbitrary paths, repository names, or deployment configuration through dispatch inputs. Dispatch selects a trusted build; it does not supply the site implementation. -- Treat workflow summaries and deployment URLs as supporting outputs, not substitutes for the report. -- Preserve issue-based reporting unless Pages materially improves navigation, history, visualization, or scale. When converting a built-in report from an issue to Pages, preserve its decision content, provenance, access expectations, and discoverability from the workflow run. - -## Control-Plane Boundary - -Pages report routing is part of the control plane, while deployment remains regular repository automation rather than an agent safe output. Agentic workflows produce source records through declared safe outputs. A conventional deterministic workflow renders and deploys the selected review or production site from trusted, durable inputs approved for that destination. Agents must not receive `pages: write`, invoke `actions/deploy-pages`, or promote their own output mode. - -The control-plane modes govern agent-created source records, not the Pages deployment: - -| Mode | Source-data behavior | -| --- | --- | -| `staged` | Stage proposed report source data only. It is not durable input to the published report. | -| `review` | Route proposed report source data to the private `safe_output_repo`, then publish it through that repository's access-controlled review Pages site. Never update production Pages. | -| `live` | Write the worker workflow's declared report source data to its normal live destination, then publish the production Pages site through its protected deterministic workflow. | - -For Pages reports, `safe_output_repo` remains the review safe-output destination and also owns the review Pages site. Require it to be private and Pages access-controlled for the intended reviewers. If access-controlled Pages is unavailable, fail review publication closed; do not expose the report publicly or silently substitute an artifact. The production Pages repository and all source locations remain fixed in trusted control-plane configuration, not selected by an agent or arbitrary dispatch input. - -Keep review and production deployments isolated with distinct repositories or environments, URLs, and concurrency groups. The deterministic publisher may run automatically after bounded source persistence or through an authorized control-plane dispatch, but the trigger must carry only fixed identifiers and validated mode. Keep target repository identity, correlation ID, central repository, source run URL, catalog or workflow identity, and generated-at time in durable source data and the rendered report when available. - -If an agent needs a new kind of durable source record, represent that write with a declared safe output before allowing the Pages workflow to consume it. Do not use `post-steps`, shell commands, or workflow dispatch as a substitute for a safe output. If immediate autonomous agent publication becomes a requirement, design and review a dedicated safe-output boundary separately rather than expanding the deterministic publisher. - -## Control-Plane Inventory Discovery - -Control-plane reports must derive their navigation and workflow inventory from the installed repository rather than a hardcoded bundle catalog. Perform repository discovery in a deterministic build step before rendering. That step emits normalized, schema-versioned inventory JSON; the static renderer consumes the prepared inventory and must not rediscover or reinterpret repository files. - -1. Recursively discover `aw.yml` manifests. Use their `name`, `description`, and workflow `includes` as package metadata, preferring the most specific nested manifest when a root catalog and a bundle manifest include the same workflow. -2. Discover agentic workflow sources from `.github/workflows/*.md`, excluding reusable files under `.github/workflows/shared/` and conventional non-agentic `.yml` workflows. -3. Match each source `<name>.md` with its generated `<name>.lock.yml`. Report source-only and lock-only entries as inventory warnings; never treat generated lock files as editable source. -4. Identify bundle orchestrators from the `shared/control.md` import with `role: orchestrator`. Treat `safe-outputs.dispatch-workflow.workflows` as the authoritative worker membership list. -5. Validate workers against discovered source files, their `role: worker` import, and stable `tracker-id` when present. Report dispatched workers that are missing or not compiled. -6. Treat remaining discovered source/lock workflow pairs as standalone workflows. Show them independently rather than dropping records that do not belong to a bundle. -7. Derive display names, descriptions, icons or emoji hints, source paths, compile state, and bundle relationships from parsed metadata. Use generic visual fallbacks when optional metadata is absent. -8. Associate durable outputs using discovered workflow IDs, tracker IDs, and display names. Do not hardcode workflow-specific issue prefixes, marker namespaces, or bundle-name regular expressions. -9. Emit the discovered inventory in a machine-readable report asset so the rendered navigation and supporting data can be audited together. Fail the render when the prepared inventory is absent or has an unsupported schema version. -10. Present review proposals separately from live production outcomes. Show the bundle's configured mode independently from the selected history view. Derive each record's mode from its attributed workflow run or trusted source route; do not add hidden mode markers to report content or display records whose mode cannot be established. - -Repository-local discovery is the required baseline and must work with the Pages job's `contents: read` permission. Organization-wide discovery across other repositories is optional and must be explicitly configured with a bounded repository inventory and credentials authorized to read those repositories. Clearly label partial or inaccessible organization results; never imply that a repository-scoped token scanned the full organization. - -## Worker Value Artifacts - -Pages consumes durable worker-value outputs; it does not execute value functions or perform evaluations during the report build. Store each worker's canonical artifacts under `.github/value/<workflow>/`: - -| Artifact | Report use | -| --- | --- | -| `<workflow>-timeline.svg` | Before/after or attainment-only plot | -| `<workflow>-timeline.json` | Structured evidence, scores, provenance, and frozen function definition | -| `<workflow>-definitions.md` | Plain-language metrics, evidence rules, direction, and limitations | -| `<workflow>-evidence-archive.json` | Function-fingerprinted valid observations retained across evaluations | - -The workflow slug in the timeline JSON and all four filename stems must match the discovered worker ID. The renderer recursively discovers `*-timeline.json` beneath `.github/value`, uses its sibling SVG for the chart, and may use the definitions and evidence archive for supporting detail. Treat these committed artifacts as trusted, durable report inputs produced by the separate value-evaluation process. Rebuild Pages when `.github/value/**` changes, but never package the evaluator into the Pages capability or regenerate evidence during publication. Show an explicit "No evaluation observations yet" state when a worker has no valid timeline and never substitute workflow run counts for operational value. - -## Validation - -Before finishing: - -1. Build the report with the same command and base path used by the Pages workflow. -2. Check for broken internal links, missing assets, invalid HTML, console errors, and unsanitized generated content. -3. Run the repository's accessibility checker when available. At minimum, inspect headings and landmarks, tab through every control, and verify labels, focus visibility, and table or chart alternatives. -4. Verify text and meaningful UI colors with an automated contrast checker in both light and dark schemes. -5. Capture desktop and mobile views. Check the first viewport, 200% zoom, long labels, empty and error states, table overflow, and overlap. -6. Verify with reduced motion, JavaScript disabled when enhancement is optional, and print preview. -7. Open the deployed or locally emulated `/<repository>/` URL and confirm canonical navigation, asset paths, provenance links, and the reported generation time. -8. Confirm staged mode does not deploy, review deploys only to access-controlled review Pages, and live deploys only to production Pages. -9. Confirm the Pages workflows are conventional Actions automation, accept no untrusted build or deployment inputs, isolate review and production environments, and grant no Pages permissions to an agent job. - -Report the generated files, publishing path, data and sanitization approach, accessibility checks, responsive viewports, and any known limitations. \ No newline at end of file diff --git a/.github/workflows/dependabot-release-train-updater.md b/.github/workflows/dependabot-release-train-updater.md index f934bf7..28c7aff 100644 --- a/.github/workflows/dependabot-release-train-updater.md +++ b/.github/workflows/dependabot-release-train-updater.md @@ -40,19 +40,28 @@ on: checkout: - repository: ${{ inputs.safe_output_repo }} + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} fetch-depth: 0 fetch: ["*"] current: true - repository: ${{ inputs.target_repo }} + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} path: target +env: + CENTRAL_AGENTIC_OPS_WORKER_ENABLED: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_UPDATER_ENABLED || 'true' }} + CENTRAL_AGENTIC_OPS_WORKER_MAX_MODE: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_UPDATER_MAX_MODE || 'staged' }} + GH_AW_SAFE_OUTPUT_MODE: ${{ inputs.safe_output_mode || 'staged' }} + REVIEW_OUTPUT_REPO: ${{ inputs.safe_output_repo || github.repository }} + SAFE_OUTPUT_REPO: ${{ inputs.safe_output_mode == 'review' && (inputs.safe_output_repo || github.repository) || '' }} + TARGET_REPO: ${{ inputs.target_repo || '' }} + imports: - uses: shared/control.md with: bundle: dependabot role: worker - worker_enabled: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_UPDATER_ENABLED || 'true' }} - worker_max_mode: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_UPDATER_MAX_MODE || 'staged' }} + allowed_owners: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} - uses: shared/review-bundle.md permissions: @@ -368,6 +377,7 @@ safe-outputs: timeout-minutes: 60 +source: githubnext/central-agentic-ops/.github/workflows/dependabot-release-train-updater.md@main --- You are a dependency reliability and supply-chain maintenance agent for the checked-out safe-output repository. @@ -400,7 +410,7 @@ Follow these rules: Read repository evidence from `target/`. Make all PR changes in the repository checked out at the workspace root, which is the safe-output repository. In `live` mode that root may be the target repository itself; in `review` mode it is the control-plane repository. Do not edit `target/` directly. -In `review` mode, do not try to make the control-plane repository look like the target repository. Treat review mode as artifact-backed review, not as a control-plane pull request. If the live outcome would be `create-pull-request`, `push-to-pull-request-branch`, or `update-pull-request`, prepare a bundle directory under `/tmp/gh-aw/review-bundles/dependabot-release-train-updater/<bundle-or-lane>/` with `summary.md`, `changed-files.txt`, `validation.txt`, and any patch or bundle files you can produce safely, then call `publish_review_bundle` with that directory and create an issue or comment in `SAFE_OUTPUT_REPO` linking the intended target repository and review guidance. +In `review` mode, do not try to make the control-plane repository look like the target repository. Treat review mode as artifact-backed review, not as a control-plane pull request. If the live outcome would be `create-pull-request`, `push-to-pull-request-branch`, or `update-pull-request`, prepare a bundle directory under `/tmp/gh-aw/agent/review-bundles/dependabot-release-train-updater/<bundle-or-lane>/` with `summary.md`, `changed-files.txt`, `validation.txt`, and any patch or bundle files you can produce safely, then call `publish_review_bundle` with that directory and create an issue or comment in `SAFE_OUTPUT_REPO` linking the intended target repository and review guidance. Files outside `/tmp/gh-aw/agent/` are not persisted to the publisher job. Treat `target_repo`, `safe_output_mode`, `safe_output_repo`, `preview_only`, `correlation_id`, `central_repo`, and `control_plane_run_url` as the live control-plane envelope. @@ -658,4 +668,4 @@ When using `noop`, include a short reason such as: - "No dependency manifests found." - "No actionable dependency update found after reviewing current open PRs and manifests." - "Potential update requires private registry credentials unavailable to this workflow." -- "All candidate updates were major or security-sensitive and should be requested explicitly." +- "All candidate updates were major or security-sensitive and should be requested explicitly." \ No newline at end of file diff --git a/.github/workflows/dependabot.md b/.github/workflows/dependabot.md index 3a6d1ab..911cdd6 100644 --- a/.github/workflows/dependabot.md +++ b/.github/workflows/dependabot.md @@ -44,20 +44,27 @@ on: - review - live +env: + CENTRAL_AGENTIC_OPS_MODE: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MODE || 'staged' }} + GH_AW_SAFE_OUTPUT_MODE: ${{ (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MODE || 'staged') == 'preview' && 'staged' || (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MODE || 'staged') }} + REVIEW_OUTPUT_REPO: ${{ inputs.safe_output_repo || github.repository }} + SAFE_OUTPUT_REPO: ${{ (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MODE || 'staged') == 'review' && (inputs.safe_output_repo || github.repository) || '' }} + TARGET_REPO: ${{ inputs.target_repo || '' }} + imports: - uses: shared/control.md with: bundle: dependabot role: orchestrator - rollout_mode: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MODE || 'staged' }} - rollout_percent: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_ROLLOUT_PERCENT || '100' }} - max_repos: ${{ vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MAX_REPOS || '1' }} + rollout_percent: ${{ inputs.rollout_percent || vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_ROLLOUT_PERCENT || '100' }} + max_repos: ${{ inputs.max_repos || vars.CENTRAL_AGENTIC_OPS_DEPENDABOT_MAX_REPOS || '1' }} max_scan_repos: ${{ vars.CENTRAL_AGENTIC_OPS_MAX_SCAN_REPOS || '1000' }} - cell_count: ${{ vars.CENTRAL_AGENTIC_OPS_CELL_COUNT || '1' }} - cell_index: ${{ vars.CENTRAL_AGENTIC_OPS_CELL_INDEX || '0' }} - batch_size: ${{ vars.CENTRAL_AGENTIC_OPS_BATCH_SIZE || '100000' }} - batch_index: ${{ vars.CENTRAL_AGENTIC_OPS_BATCH_INDEX || '0' }} + cell_count: ${{ inputs.cell_count || vars.CENTRAL_AGENTIC_OPS_CELL_COUNT || '1' }} + cell_index: ${{ inputs.cell_index || vars.CENTRAL_AGENTIC_OPS_CELL_INDEX || '0' }} + batch_size: ${{ inputs.batch_size || vars.CENTRAL_AGENTIC_OPS_BATCH_SIZE || '100000' }} + batch_index: ${{ inputs.batch_index || vars.CENTRAL_AGENTIC_OPS_BATCH_INDEX || '0' }} allowed_owners: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} + allowed_repos: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_REPOS || '' }} dispatch_max: "50" orchestrator_credits: "250" worker_credits_per_target: "600" @@ -89,6 +96,7 @@ safe-outputs: workflows: [dependabot-release-train-updater] max: 50 +source: githubnext/central-agentic-ops@2de9130ff1709fccdacbe5261fd5da71995e6721 --- # Dependabot diff --git a/.github/workflows/optimization-ai-credit-auditor.md b/.github/workflows/optimization-ai-credit-auditor.md index f631d13..c6a62d6 100644 --- a/.github/workflows/optimization-ai-credit-auditor.md +++ b/.github/workflows/optimization-ai-credit-auditor.md @@ -32,19 +32,28 @@ on: checkout: - repository: ${{ inputs.safe_output_repo }} + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} fetch-depth: 0 fetch: ["*"] current: true - repository: ${{ inputs.target_repo }} + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} path: target +env: + CENTRAL_AGENTIC_OPS_WORKER_ENABLED: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_AUDITOR_ENABLED || 'true' }} + CENTRAL_AGENTIC_OPS_WORKER_MAX_MODE: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_AUDITOR_MAX_MODE || 'staged' }} + GH_AW_SAFE_OUTPUT_MODE: ${{ inputs.safe_output_mode || 'staged' }} + REVIEW_OUTPUT_REPO: ${{ inputs.safe_output_repo || github.repository }} + SAFE_OUTPUT_REPO: ${{ inputs.safe_output_mode == 'review' && (inputs.safe_output_repo || github.repository) || '' }} + TARGET_REPO: ${{ inputs.target_repo || '' }} + imports: - uses: shared/control.md with: bundle: optimization role: worker - worker_enabled: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_AUDITOR_ENABLED || 'true' }} - worker_max_mode: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_AUDITOR_MAX_MODE || 'staged' }} + allowed_owners: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} permissions: contents: read @@ -180,6 +189,7 @@ steps: echo '{"runs":[],"summary":{}}' > /tmp/gh-aw/token-audit/workflow-logs.json fi +source: githubnext/central-agentic-ops/.github/workflows/optimization-ai-credit-auditor.md@main --- You are the Agentic Workflow Auditor — a workflow that tracks daily AI Credit (AIC) spend and token consumption across all agentic workflows in the target repository and maintains a historical record for trend analysis. @@ -410,4 +420,4 @@ if (fs.existsSync(assignmentsFile)) { This enables filtering workflow runs by experiment variant in Datadog, Honeycomb, or any OTLP-compatible backend. Attribute keys follow the pattern `gh_aw.experiment.<name>` with the -assigned variant as the value, plus `gh_aw.experiment.names` as a comma-separated index. +assigned variant as the value, plus `gh_aw.experiment.names` as a comma-separated index. \ No newline at end of file diff --git a/.github/workflows/optimization-ai-credit-optimizer.md b/.github/workflows/optimization-ai-credit-optimizer.md index 27dfc63..848f112 100644 --- a/.github/workflows/optimization-ai-credit-optimizer.md +++ b/.github/workflows/optimization-ai-credit-optimizer.md @@ -30,14 +30,20 @@ on: batch_label: type: string +env: + CENTRAL_AGENTIC_OPS_WORKER_ENABLED: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_OPTIMIZER_ENABLED || 'true' }} + CENTRAL_AGENTIC_OPS_WORKER_MAX_MODE: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_OPTIMIZER_MAX_MODE || 'staged' }} + GH_AW_SAFE_OUTPUT_MODE: ${{ inputs.safe_output_mode || 'staged' }} + REVIEW_OUTPUT_REPO: ${{ inputs.safe_output_repo || github.repository }} + SAFE_OUTPUT_REPO: ${{ inputs.safe_output_mode == 'review' && (inputs.safe_output_repo || github.repository) || '' }} + TARGET_REPO: ${{ inputs.target_repo || '' }} imports: - uses: shared/control.md with: bundle: optimization role: worker - worker_enabled: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_OPTIMIZER_ENABLED || 'true' }} - worker_max_mode: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_OPTIMIZER_MAX_MODE || 'staged' }} + allowed_owners: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} - uses: shared/target-checkout-read-org-token.md permissions: @@ -240,6 +246,7 @@ steps: echo "ℹ️ No previous optimization history found." fi +source: githubnext/central-agentic-ops/.github/workflows/optimization-ai-credit-optimizer.md@main --- You are the Agentic Workflow Optimizer. Pick one high AI credit workflow, audit recent runs, and create a conservative optimization issue with measurable improvements. Your recommendations may include prompt, tool, reliability, setup-prefix, and inline sub-agent improvements when the evidence supports them. @@ -417,4 +424,4 @@ description: Filter run data for a target workflow and compute AI credit and tim --- You are a run statistics aggregation assistant. You receive the target workflow name. -Use `jq` to aggregate from `/tmp/gh-aw/token-audit/all-runs.json` (filtering within `.runs`) without printing raw run JSON. Compute total/avg/min/max AIC, action-minutes total/P50/P90, and conclusion counts for the target workflow, and output exactly one markdown table with columns: Metric | Value. +Use `jq` to aggregate from `/tmp/gh-aw/token-audit/all-runs.json` (filtering within `.runs`) without printing raw run JSON. Compute total/avg/min/max AIC, action-minutes total/P50/P90, and conclusion counts for the target workflow, and output exactly one markdown table with columns: Metric | Value. \ No newline at end of file diff --git a/.github/workflows/optimization.md b/.github/workflows/optimization.md index d849a64..656bdab 100644 --- a/.github/workflows/optimization.md +++ b/.github/workflows/optimization.md @@ -44,20 +44,27 @@ on: - review - live +env: + CENTRAL_AGENTIC_OPS_MODE: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MODE || 'staged' }} + GH_AW_SAFE_OUTPUT_MODE: ${{ (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MODE || 'staged') == 'preview' && 'staged' || (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MODE || 'staged') }} + REVIEW_OUTPUT_REPO: ${{ inputs.safe_output_repo || github.repository }} + SAFE_OUTPUT_REPO: ${{ (inputs.safe_output_mode || vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MODE || 'staged') == 'review' && (inputs.safe_output_repo || github.repository) || '' }} + TARGET_REPO: ${{ inputs.target_repo || '' }} + imports: - uses: shared/control.md with: bundle: optimization role: orchestrator - rollout_mode: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MODE || 'staged' }} - rollout_percent: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_ROLLOUT_PERCENT || '100' }} - max_repos: ${{ vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MAX_REPOS || '1' }} + rollout_percent: ${{ inputs.rollout_percent || vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_ROLLOUT_PERCENT || '100' }} + max_repos: ${{ inputs.max_repos || vars.CENTRAL_AGENTIC_OPS_OPTIMIZATION_MAX_REPOS || '1' }} max_scan_repos: ${{ vars.CENTRAL_AGENTIC_OPS_MAX_SCAN_REPOS || '1000' }} - cell_count: ${{ vars.CENTRAL_AGENTIC_OPS_CELL_COUNT || '1' }} - cell_index: ${{ vars.CENTRAL_AGENTIC_OPS_CELL_INDEX || '0' }} - batch_size: ${{ vars.CENTRAL_AGENTIC_OPS_BATCH_SIZE || '100000' }} - batch_index: ${{ vars.CENTRAL_AGENTIC_OPS_BATCH_INDEX || '0' }} + cell_count: ${{ inputs.cell_count || vars.CENTRAL_AGENTIC_OPS_CELL_COUNT || '1' }} + cell_index: ${{ inputs.cell_index || vars.CENTRAL_AGENTIC_OPS_CELL_INDEX || '0' }} + batch_size: ${{ inputs.batch_size || vars.CENTRAL_AGENTIC_OPS_BATCH_SIZE || '100000' }} + batch_index: ${{ inputs.batch_index || vars.CENTRAL_AGENTIC_OPS_BATCH_INDEX || '0' }} allowed_owners: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} + allowed_repos: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_REPOS || '' }} dispatch_max: "20" orchestrator_credits: "250" worker_credits_per_target: "850" @@ -85,6 +92,7 @@ safe-outputs: workflows: [optimization-ai-credit-auditor, optimization-ai-credit-optimizer] max: 20 +source: githubnext/central-agentic-ops@2de9130ff1709fccdacbe5261fd5da71995e6721 --- # Optimization diff --git a/.github/workflows/shared/control-precompute.md b/.github/workflows/shared/control-precompute.md index 3fe549c..f6f18cc 100644 --- a/.github/workflows/shared/control-precompute.md +++ b/.github/workflows/shared/control-precompute.md @@ -34,6 +34,9 @@ import-schema: allowed_owners: type: string default: "" + allowed_repos: + type: string + default: "" dispatch_max: type: string default: "1" @@ -97,6 +100,7 @@ steps: BATCH_SIZE: ${{ github.aw.import-inputs.batch_size }} BATCH_INDEX: ${{ github.aw.import-inputs.batch_index }} ALLOWED_OWNERS: ${{ github.aw.import-inputs.allowed_owners }} + ALLOWED_REPOS: ${{ github.aw.import-inputs.allowed_repos }} DISPATCH_MAX: ${{ github.aw.import-inputs.dispatch_max }} ROLLOUT_PERCENT: ${{ github.aw.import-inputs.rollout_percent }} SAFE_OUTPUT_MODE: ${{ github.aw.import-inputs.safe_output_mode }} @@ -280,6 +284,31 @@ steps: exit 1 } + prepare_allowlist() { + local allowed_repo + + : > /tmp/gh-aw/agent/allowed-repos + [ -z "$ALLOWED_REPOS" ] && return + + if ! printf '%s' "$ALLOWED_REPOS" | jq -Rr \ + 'split(",") | map(gsub("\\s"; "") | ascii_downcase) | unique[]' \ + > /tmp/gh-aw/agent/allowed-repos || grep -qx '' /tmp/gh-aw/agent/allowed-repos; then + echo "CENTRAL_AGENTIC_OPS_ALLOWED_REPOS is invalid" >&2 + exit 1 + fi + while read -r allowed_repo; do + validate_repository_owner "allowed repository" "$allowed_repo" + done < /tmp/gh-aw/agent/allowed-repos + if [ "$(wc -l < /tmp/gh-aw/agent/allowed-repos)" -gt "$MAX_SCAN_REPOS" ]; then + echo "allowed repos exceed max_scan_repos" >&2 + exit 1 + fi + if [ -n "$TARGET_REPO" ] && ! grep -Fqix "$TARGET_REPO" /tmp/gh-aw/agent/allowed-repos; then + echo "target_repo is not allowed" >&2 + exit 1 + fi + } + derive_control_source_path() { workflow_ref_path="${GITHUB_WORKFLOW_REF#${GITHUB_REPOSITORY}/}" workflow_path="${workflow_ref_path%@*}" @@ -335,6 +364,12 @@ steps: return fi + if [ -n "$ALLOWED_REPOS" ]; then + repo_source="allowed_repos" + load_allowed_inventory + return + fi + if ! load_bounded_inventory "orgs/$ORGANIZATION/repos" "all"; then if ! load_bounded_inventory "users/$ORGANIZATION/repos" "owner"; then repo_error=$(cat /tmp/gh-aw/agent/repo-error.txt) @@ -343,6 +378,25 @@ steps: fi } + load_allowed_inventory() { + local allowed_repo + + printf '[]\n' > /tmp/gh-aw/agent/candidates.json + : > /tmp/gh-aw/agent/candidate-pages.jsonl + while read -r allowed_repo; do + if ! gh api "repos/$allowed_repo" \ + --jq '{id, full_name, archived, disabled, private, pushed_at, default_branch}' \ + >> /tmp/gh-aw/agent/candidate-pages.jsonl 2>/tmp/gh-aw/agent/repo-error.txt; then + repo_error="cannot read allowed repository $allowed_repo" + printf '[]\n' > /tmp/gh-aw/agent/candidates.json + return + fi + done < /tmp/gh-aw/agent/allowed-repos + + jq -s '.' /tmp/gh-aw/agent/candidate-pages.jsonl \ + > /tmp/gh-aw/agent/candidates.json + } + load_bounded_inventory() { local endpoint="$1" local repository_type="$2" @@ -592,6 +646,7 @@ steps: exit 0 fi + prepare_allowlist write_orchestrator_precompute --- diff --git a/.github/workflows/shared/control.md b/.github/workflows/shared/control.md index 0c85ca1..49f4c48 100644 --- a/.github/workflows/shared/control.md +++ b/.github/workflows/shared/control.md @@ -34,6 +34,9 @@ import-schema: allowed_owners: type: string default: "" + allowed_repos: + type: string + default: "" dispatch_max: type: string default: "1" @@ -53,13 +56,6 @@ import-schema: type: string default: "1100" -env: - CENTRAL_AGENTIC_OPS_MODE: ${{ github.aw.import-inputs.rollout_mode == 'preview' && 'staged' || github.aw.import-inputs.rollout_mode }} - GH_AW_SAFE_OUTPUT_MODE: ${{ (github.event.inputs.safe_output_mode || github.aw.import-inputs.rollout_mode || 'staged') == 'preview' && 'staged' || (github.event.inputs.safe_output_mode || github.aw.import-inputs.rollout_mode || 'staged') }} - TARGET_REPO: ${{ github.event.inputs.target_repo || '' }} - REVIEW_OUTPUT_REPO: ${{ github.event.inputs.safe_output_repo || github.repository }} - SAFE_OUTPUT_REPO: ${{ (github.event.inputs.safe_output_mode || github.aw.import-inputs.rollout_mode || 'staged') == 'review' && env.REVIEW_OUTPUT_REPO || '' }} - github-app: client-id: ${{ vars.GH_AW_GITHUB_APP_ID }} private-key: ${{ secrets.GH_AW_GITHUB_APP_PRIVATE_KEY }} @@ -75,27 +71,28 @@ imports: role: ${{ github.aw.import-inputs.role }} target_repo: ${{ github.event.inputs.target_repo || '' }} organization: ${{ github.repository_owner }} - max_repos: ${{ github.event.inputs.max_repos || github.aw.import-inputs.max_repos || '1' }} - max_scan_repos: ${{ github.aw.import-inputs.max_scan_repos || '1000' }} - cell_count: ${{ github.event.inputs.cell_count || github.aw.import-inputs.cell_count || '1' }} - cell_index: ${{ github.event.inputs.cell_index || github.aw.import-inputs.cell_index || '0' }} - batch_size: ${{ github.event.inputs.batch_size || github.aw.import-inputs.batch_size || '100000' }} - batch_index: ${{ github.event.inputs.batch_index || github.aw.import-inputs.batch_index || '0' }} - allowed_owners: ${{ github.aw.import-inputs.allowed_owners || vars.CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS || github.repository_owner }} - dispatch_max: ${{ github.aw.import-inputs.dispatch_max || '1' }} - rollout_percent: ${{ github.event.inputs.rollout_percent || github.aw.import-inputs.rollout_percent || '100' }} + max_repos: "${{ github.aw.import-inputs.max_repos }}" + max_scan_repos: "${{ github.aw.import-inputs.max_scan_repos }}" + cell_count: "${{ github.aw.import-inputs.cell_count }}" + cell_index: "${{ github.aw.import-inputs.cell_index }}" + batch_size: "${{ github.aw.import-inputs.batch_size }}" + batch_index: "${{ github.aw.import-inputs.batch_index }}" + allowed_owners: "${{ github.aw.import-inputs.allowed_owners }}" + allowed_repos: "${{ github.aw.import-inputs.allowed_repos }}" + dispatch_max: "${{ github.aw.import-inputs.dispatch_max }}" + rollout_percent: "${{ github.aw.import-inputs.rollout_percent }}" safe_output_mode: ${{ env.GH_AW_SAFE_OUTPUT_MODE }} safe_output_repo: ${{ env.SAFE_OUTPUT_REPO }} preview_only: ${{ (env.GH_AW_SAFE_OUTPUT_MODE == 'live' || env.GH_AW_SAFE_OUTPUT_MODE == 'review') && 'false' || 'true' }} enabled: ${{ github.event_name == 'workflow_dispatch' || env.CENTRAL_AGENTIC_OPS_MODE == 'staged' || env.CENTRAL_AGENTIC_OPS_MODE == 'review' || env.CENTRAL_AGENTIC_OPS_MODE == 'live' }} - worker_enabled: ${{ github.aw.import-inputs.worker_enabled || 'true' }} - worker_max_mode: ${{ github.aw.import-inputs.worker_max_mode || 'staged' }} + worker_enabled: ${{ env.CENTRAL_AGENTIC_OPS_WORKER_ENABLED || 'true' }} + worker_max_mode: ${{ env.CENTRAL_AGENTIC_OPS_WORKER_MAX_MODE || 'staged' }} correlation_id: ${{ github.event.inputs.correlation_id || '' }} central_repo: ${{ github.event.inputs.central_repo || '' }} control_plane_run_url: ${{ github.event.inputs.control_plane_run_url || '' }} - orchestrator_credits: ${{ github.aw.import-inputs.orchestrator_credits || '0' }} - worker_credits_per_target: ${{ github.aw.import-inputs.worker_credits_per_target || '0' }} - aggregate_credit_limit: ${{ github.aw.import-inputs.aggregate_credit_limit || '1100' }} + orchestrator_credits: "${{ github.aw.import-inputs.orchestrator_credits }}" + worker_credits_per_target: "${{ github.aw.import-inputs.worker_credits_per_target }}" + aggregate_credit_limit: "${{ github.aw.import-inputs.aggregate_credit_limit }}" --- Read `/tmp/gh-aw/agent/control-precompute.json` before making control decisions. Treat it as authoritative for `control_role`, enablement state, target repository inputs, safe-output routing, and worker workflow availability. diff --git a/.github/workflows/shared/review-bundle.md b/.github/workflows/shared/review-bundle.md index d582039..c543832 100644 --- a/.github/workflows/shared/review-bundle.md +++ b/.github/workflows/shared/review-bundle.md @@ -66,20 +66,23 @@ safe-outputs: exit 1 fi - if [[ "$SOURCE_DIR_RAW" = /* ]]; then - SOURCE_DIR=$(realpath -m "$SOURCE_DIR_RAW") - else - SOURCE_DIR=$(realpath -m "$GITHUB_WORKSPACE/$SOURCE_DIR_RAW") + PERSISTED_ROOT="/tmp/gh-aw/agent/review-bundles" + if [[ "$SOURCE_DIR_RAW" != "$PERSISTED_ROOT"/* ]]; then + echo "source_dir must be under $PERSISTED_ROOT: $SOURCE_DIR_RAW" >&2 + exit 1 fi - WORKSPACE_ROOT=$(realpath -m "$GITHUB_WORKSPACE") - if [[ "$SOURCE_DIR" != "$WORKSPACE_ROOT"/* && "$SOURCE_DIR" != /tmp/* ]]; then - echo "source_dir must be under the workspace or /tmp: $SOURCE_DIR" >&2 + ARTIFACT_ROOT=$(realpath -m "$(dirname "$GH_AW_AGENT_OUTPUT")") + SOURCE_SUFFIX=${SOURCE_DIR_RAW#"/tmp/gh-aw/agent/"} + SOURCE_DIR=$(realpath -m "$ARTIFACT_ROOT/agent/$SOURCE_SUFFIX") + RESTORED_ROOT=$(realpath -m "$ARTIFACT_ROOT/agent/review-bundles") + if [[ "$SOURCE_DIR" != "$RESTORED_ROOT"/* ]]; then + echo "source_dir escapes the restored review bundle root: $SOURCE_DIR_RAW" >&2 exit 1 fi if [ ! -d "$SOURCE_DIR" ]; then - echo "source_dir does not exist: $SOURCE_DIR" >&2 + echo "review bundle was not persisted in the agent artifact: $SOURCE_DIR_RAW" >&2 exit 1 fi diff --git a/.github/workflows/shared/target-checkout-read-org-token.md b/.github/workflows/shared/target-checkout-read-org-token.md index d1b01ad..3f51fc3 100644 --- a/.github/workflows/shared/target-checkout-read-org-token.md +++ b/.github/workflows/shared/target-checkout-read-org-token.md @@ -1,5 +1,6 @@ --- checkout: repository: ${{ inputs.target_repo }} + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} current: true --- \ No newline at end of file diff --git a/docs/operations.md b/docs/operations.md index a285d02..8bec55c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -115,14 +115,17 @@ Observability imports for Sentry, Grafana, and Datadog are shared control-plane ### Activating Pages -Pages is not part of the core catalog. After verifying that the control repository is private and its Pages site is access-controlled, install the view explicitly: +Pages is not part of the Agentic Workflow package catalog. After verifying that the control repository is private and its Pages site is access-controlled, copy the conventional workflow and report scripts from a checkout pinned to the desired catalog release or commit: ```bash -gh aw add-wizard githubnext/central-agentic-ops/pages@<catalog-release> +control_repository=/path/to/control-repository +mkdir -p "$control_repository/.github/workflows" "$control_repository/.github/scripts/pages-report" +cp pages/pages.yml "$control_repository/.github/workflows/pages.yml" +cp .github/scripts/pages-report/*.mjs "$control_repository/.github/scripts/pages-report/" ``` :::note[Do not create `REPORT_PAGES_TOKEN`] -The Pages bundle does not use a `REPORT_PAGES_TOKEN` secret. Its build job reads report data with the automatic `github.token` and explicit job-scoped permissions. Its deploy job uses GitHub Pages OIDC with `pages: write` and `id-token: write`. If an installed workflow requests `REPORT_PAGES_TOKEN`, it did not come from the current catalog release and should be reviewed or updated rather than supplied with a PAT. +The Pages publisher does not use a `REPORT_PAGES_TOKEN` secret. Its build job reads report data with the automatic `github.token` and explicit job-scoped permissions. Its deploy job uses GitHub Pages OIDC with `pages: write` and `id-token: write`. If a copied workflow requests `REPORT_PAGES_TOKEN`, it did not come from the current catalog release and should be reviewed or updated rather than supplied with a PAT. ::: :::caution[The report can contain private repository data] @@ -136,11 +139,12 @@ A deliberate custom extension should mint a short-lived GitHub App token install The add-on installs the following report components in the control-plane repository: - `.github/workflows/pages.yml`, the conventional build and deployment workflow; -- `.github/skills/github-pages-report/SKILL.md`, the report authoring and review guidance; -- `.github/skills/github-pages-report/inventory.mjs`, the dependency-free control-plane inventory extractor; -- `.github/skills/github-pages-report/report.mjs`, the trusted static renderer. +- `.github/scripts/pages-report/aic-usage.mjs`, the bounded AI Credit usage collector; +- `.github/scripts/pages-report/deployed-workflows.mjs`, the deployed workflow and run-health collector; +- `.github/scripts/pages-report/inventory.mjs`, the dependency-free control-plane inventory extractor; +- `.github/scripts/pages-report/report.mjs`, the trusted static renderer. -After running `gh aw add-wizard githubnext/central-agentic-ops@<catalog-release>`: +After copying the report files from the pinned catalog checkout: 1. Commit and push the installed files. 2. In **Settings > Pages**, select **GitHub Actions** as the source and apply the required access controls. diff --git a/pages/README.md b/pages/README.md index 0b033b3..521aa3f 100644 --- a/pages/README.md +++ b/pages/README.md @@ -1,9 +1,9 @@ -# Pages Bundle +# Pages Add-on > [!WARNING] > This project is experimental and not ready for use. -The Pages bundle publishes an access-controlled static view of Central Agentic Ops reports from a private control-plane repository. +The Pages add-on publishes an access-controlled static view of Central Agentic Ops reports from a private control-plane repository. > [!NOTE] > Do not create a `REPORT_PAGES_TOKEN` secret. The workflow reads report data with the automatic `github.token` under explicit job permissions and deploys through GitHub Pages OIDC using `pages: write` and `id-token: write`. @@ -14,18 +14,23 @@ The Pages bundle publishes an access-controlled static view of Central Agentic O ## Contents - `pages.yml`: deterministic GitHub Pages build and deployment workflow. -- `github-pages-report`: report generation, accessibility, and publishing guidance. +- `.github/scripts/pages-report`: deterministic inventory, AI Credit collection, and static report generation scripts. The publisher reads trusted workflow, issue, pull request, and value-artifact data from the installed repository. AI agents do not receive `pages: write`, `id-token: write`, or deployment authority. ## Install -Install the bundle in the private control-plane or review repository that will own the Pages site: +From a checkout of the desired catalog release, copy the conventional workflow and report scripts into the private control-plane or review repository that will own the Pages site: ```bash -gh aw add-wizard githubnext/central-agentic-ops/pages@<catalog-release> +control_repository=/path/to/control-repository +mkdir -p "$control_repository/.github/workflows" "$control_repository/.github/scripts/pages-report" +cp pages/pages.yml "$control_repository/.github/workflows/pages.yml" +cp .github/scripts/pages-report/*.mjs "$control_repository/.github/scripts/pages-report/" ``` +These files are conventional repository automation rather than an Agentic Workflow package. Pin the catalog checkout to a reviewed release or commit before copying them. + ## Configure 1. In **Settings > Pages**, select **GitHub Actions** as the source. @@ -33,4 +38,4 @@ gh aw add-wizard githubnext/central-agentic-ops/pages@<catalog-release> 3. Protect the `github-pages` environment as required by your organization. 4. Run **Pages** from the repository's **Actions** page. -Do not use this bundle when the report would be public or when the repository plan cannot enforce the required access boundary. See [Publishing Pages Reports](../docs/operations.md#publishing-pages-reports) for operating details. +Do not use this add-on when the report would be public or when the repository plan cannot enforce the required access boundary. See [Publishing Pages Reports](../docs/operations.md#publishing-pages-reports) for operating details. diff --git a/pages/aw.yml b/pages/aw.yml deleted file mode 100644 index 90548a4..0000000 --- a/pages/aw.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: Pages -description: Optional access-controlled GitHub Pages view for Central Agentic Ops reports. -min-version: v0.87.2 -includes: - - source: pages.yml - destination: .github/workflows/pages.yml - kind: action-workflow - - .github/skills/github-pages-report \ No newline at end of file diff --git a/pages/pages.yml b/pages/pages.yml index 6467cd4..3887774 100644 --- a/pages/pages.yml +++ b/pages/pages.yml @@ -2,30 +2,8 @@ name: Pages on: workflow_dispatch: - push: - paths: - - "**/aw.yml" - - .github/skills/github-pages-report/inventory.mjs - - .github/skills/github-pages-report/deployed-workflows.mjs - - .github/skills/github-pages-report/report.mjs - - .github/value/** - - .github/workflows/*.md - - .github/workflows/*.lock.yml - - .github/workflows/pages.yml schedule: - cron: "23 5 * * *" - issues: - types: [opened, edited, closed, reopened, deleted] - issue_comment: - types: [created, edited, deleted] - pull_request: - types: [opened, edited, closed, reopened, synchronize, converted_to_draft, ready_for_review] - workflow_run: - workflows: - - Dependabot / Release Train Updater - - Optimization / AI Credit Auditor - - Optimization / AI Credit Optimizer - types: [completed] permissions: contents: read @@ -57,22 +35,55 @@ jobs: - name: Extract control-plane inventory env: REPORT_INVENTORY: ${{ runner.temp }}/control-plane-inventory.json - run: node .github/skills/github-pages-report/inventory.mjs + run: node .github/scripts/pages-report/inventory.mjs - name: Discover deployed agentic workflows env: GITHUB_TOKEN: ${{ github.token }} + REPORT_ALLOWED_REPOS: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_REPOS }} REPORT_DEPLOYED_WORKFLOWS: ${{ runner.temp }}/deployed-workflows.json - run: node .github/skills/github-pages-report/deployed-workflows.mjs + run: node .github/scripts/pages-report/deployed-workflows.mjs - - name: Build bundle report + - name: Install gh-aw CLI + uses: github/gh-aw-actions/setup-cli@b304200a0ef4b3998673bfc7945acb08ab8c88b7 # v0.87.2 + with: + version: v0.87.2 + + - name: Restore AI Credit usage cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .cache/pages-aic + key: ${{ runner.os }}-pages-aic-${{ github.repository }}-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-pages-aic-${{ github.repository }}- + + - name: Collect AI Credit usage + env: + GH_TOKEN: ${{ github.token }} + REPORT_AIC_CACHE: .cache/pages-aic + REPORT_AIC_CONCURRENCY: "3" + REPORT_AIC_USAGE: ${{ runner.temp }}/aic-usage.json + REPORT_DEPLOYED_WORKFLOWS: ${{ runner.temp }}/deployed-workflows.json + run: node .github/scripts/pages-report/aic-usage.mjs + + - name: Save AI Credit usage cache + if: always() + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .cache/pages-aic + key: ${{ runner.os }}-pages-aic-${{ github.repository }}-${{ github.run_id }} + + - name: Build operations report env: GITHUB_TOKEN: ${{ github.token }} + REPORT_ALLOWED_REPOS: ${{ vars.CENTRAL_AGENTIC_OPS_ALLOWED_REPOS }} + REPORT_REPOSITORY_VARIABLES: ${{ toJSON(vars) }} + REPORT_AIC_USAGE: ${{ runner.temp }}/aic-usage.json REPORT_INVENTORY: ${{ runner.temp }}/control-plane-inventory.json REPORT_DEPLOYED_WORKFLOWS: ${{ runner.temp }}/deployed-workflows.json REPORT_OUTPUT: _site REPORT_VALUE_ROOT: .github/value - run: node .github/skills/github-pages-report/report.mjs + run: node .github/scripts/pages-report/report.mjs - name: Upload Pages artifact uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 diff --git a/tests/integration/package-lifecycle.test.mjs b/tests/integration/package-lifecycle.test.mjs index 7e04e75..e79994e 100644 --- a/tests/integration/package-lifecycle.test.mjs +++ b/tests/integration/package-lifecycle.test.mjs @@ -21,10 +21,6 @@ const expectedFiles = [ ".github/agents/agentic-workflows.md", ".github/skills/agentic-workflows/SKILL.md", ".github/skills/create-ops-bundle/SKILL.md", - ".github/skills/github-pages-report/SKILL.md", - ".github/skills/github-pages-report/deployed-workflows.mjs", - ".github/skills/github-pages-report/inventory.mjs", - ".github/skills/github-pages-report/report.mjs", ".github/workflows/dependabot-release-train-updater.md", ".github/workflows/dependabot.md", ".github/workflows/optimization-ai-credit-auditor.md", diff --git a/tests/load/control-plane-load.test.mjs b/tests/load/control-plane-load.test.mjs index 9508f1a..4c6bce4 100644 --- a/tests/load/control-plane-load.test.mjs +++ b/tests/load/control-plane-load.test.mjs @@ -54,6 +54,7 @@ function runPrecompute(overrides = {}) { TARGET_REPO: "", MAX_REPOS: "1000", MAX_SCAN_REPOS: "100000", + ALLOWED_REPOS: "", DISPATCH_MAX: "1000", ROLLOUT_PERCENT: "10", WORKER_CREDITS_PER_TARGET: "0", diff --git a/tests/unit/workflow-contract.test.mjs b/tests/unit/workflow-contract.test.mjs index e5e721b..c36b11d 100644 --- a/tests/unit/workflow-contract.test.mjs +++ b/tests/unit/workflow-contract.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { cpSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import test from "node:test"; @@ -244,11 +244,14 @@ test("enterprise defaults, budgets, timeouts, and concurrency are finite", () => const control = workflow("shared/control.md"); const precompute = workflow("shared/control-precompute.md"); - assert.match(control, /max_repos:.*github\.aw\.import-inputs\.max_repos \|\| '1'/); - assert.match(control, /max_scan_repos:.*github\.aw\.import-inputs\.max_scan_repos \|\| '1000'/); - assert.match(control, /CENTRAL_AGENTIC_OPS_ALLOWED_OWNERS \|\| github\.repository_owner/); + assert.match(control, /max_repos: "\$\{\{ github\.aw\.import-inputs\.max_repos \}\}"/); + assert.match(control, /max_scan_repos: "\$\{\{ github\.aw\.import-inputs\.max_scan_repos \}\}"/); + assert.match(control, /allowed_owners: "\$\{\{ github\.aw\.import-inputs\.allowed_owners \}\}"/); + assert.match(control, /allowed_repos: "\$\{\{ github\.aw\.import-inputs\.allowed_repos \}\}"/); assert.match(precompute, /max_repos must be an integer from 1 through 1000/); assert.match(precompute, /max_scan_repos must be an integer from 1 through 100000/); + assert.match(precompute, /CENTRAL_AGENTIC_OPS_ALLOWED_REPOS is invalid/); + assert.match(precompute, /repo_source="allowed_repos"/); assert.match(precompute, /inventory_version/); assert.match(precompute, /batch_id/); assert.match(precompute, /\.id % \$cell_count/); @@ -505,12 +508,16 @@ test("shared control keeps manual and scheduled routing event-scoped", () => { const control = workflow("shared/control.md"); const precompute = workflow("shared/control-precompute.md"); - assert.match(control, /github\.aw\.import-inputs\.rollout_mode == 'preview' && 'staged'/); - assert.match(control, /github\.event\.inputs\.safe_output_mode \|\| github\.aw\.import-inputs\.rollout_mode \|\| 'staged'/); - assert.match(control, /github\.event\.inputs\.safe_output_repo \|\| github\.repository/); + for (const name of ["dependabot.md", "optimization.md"]) { + const orchestrator = workflow(name); + assert.match(orchestrator, /GH_AW_SAFE_OUTPUT_MODE:.*== 'preview' && 'staged'/); + assert.match(orchestrator, /REVIEW_OUTPUT_REPO:.*inputs\.safe_output_repo \|\| github\.repository/); + assert.match(orchestrator, /SAFE_OUTPUT_REPO:.*== 'review'/); + } + assert.match(control, /safe_output_mode: \$\{\{ env\.GH_AW_SAFE_OUTPUT_MODE \}\}/); + assert.match(control, /safe_output_repo: \$\{\{ env\.SAFE_OUTPUT_REPO \}\}/); assert.doesNotMatch(control, /review_repo/); - assert.match(control, /rollout_percent: \$\{\{ github\.event\.inputs\.rollout_percent \|\| github\.aw\.import-inputs\.rollout_percent \|\| '100' \}\}/); - assert.match(control, /== 'review' && env\.REVIEW_OUTPUT_REPO/); + assert.match(control, /rollout_percent: "\$\{\{ github\.aw\.import-inputs\.rollout_percent \}\}"/); assert.match(control, /GH_AW_SAFE_OUTPUT_MODE == 'live'.*GH_AW_SAFE_OUTPUT_MODE == 'review'.*'false' \|\| 'true'/); assert.match(control, /select no more than `effective_max_repos` repositories/); @@ -544,8 +551,9 @@ test("every worker uses the standard dispatch envelope and safe mode vocabulary" assert.match(source, /safe-outputs:\n\s+staged: \$\{\{ inputs\.preview_only == 'true' \}\}/); assert.doesNotMatch(source, /safe_output_mode == 'private'/); - assert.match(source, /worker_enabled:.*\|\| 'true'/); - assert.match(source, /worker_max_mode:.*\|\| 'staged'/); + assert.match(source, /CENTRAL_AGENTIC_OPS_WORKER_ENABLED:.*\|\| 'true'/); + assert.match(source, /CENTRAL_AGENTIC_OPS_WORKER_MAX_MODE:.*\|\| 'staged'/); + assert.match(source, /GH_AW_SAFE_OUTPUT_MODE: \$\{\{ inputs\.safe_output_mode \|\| 'staged' \}\}/); for (const line of source.match(/^\s+target-repo:.*$/gm) || []) { assert.match(line, /github\.event\.inputs\.safe_output_repo/); @@ -602,8 +610,6 @@ test("clean-room compilation emits the expected GitHub Actions settings", { time for (const name of lockNames) { const generated = workflow(name, generatedDirectory); - assert.match(generated, /GH_AW_SAFE_OUTPUT_MODE:.*== 'preview' && 'staged'/); - assert.match(generated, /ROLLOUT_PERCENT: \$\{\{ github\.event\.inputs\.rollout_percent \|\| github\.aw\.import-inputs\.rollout_percent \|\| '100' \}\}/); assert.match(generated, /effective_max_repos/); assert.match(generated, /rollout_percent must be an integer from 1 through 100/); assert.match(generated, /max_repos must be an integer from 1 through 1000/); @@ -616,6 +622,8 @@ test("clean-room compilation emits the expected GitHub Actions settings", { time for (const name of ["dependabot.lock.yml", "optimization.lock.yml"]) { const generated = workflow(name, generatedDirectory); + assert.match(generated, /GH_AW_SAFE_OUTPUT_MODE:.*== 'preview' && 'staged'/); + assert.match(generated, /ROLLOUT_PERCENT: \$\{\{ inputs\.rollout_percent \|\| vars\.CENTRAL_AGENTIC_OPS_.+_ROLLOUT_PERCENT \|\| '100' \}\}/); assert.match(generated, /rollout_percent:\n\s+default: 100\n\s+type: number/); assert.match(generated, /timeout-minutes: 15/); assert.match(generated, /cancel-in-progress: true/); @@ -623,6 +631,8 @@ test("clean-room compilation emits the expected GitHub Actions settings", { time for (const name of expectedLockNames.filter((name) => !["dependabot.lock.yml", "optimization.lock.yml"].includes(name))) { const generated = workflow(name, generatedDirectory); + assert.match(generated, /GH_AW_SAFE_OUTPUT_MODE: \$\{\{ inputs\.safe_output_mode \|\| 'staged' \}\}/); + assert.match(generated, /ROLLOUT_PERCENT: "100"/); assert.match(generated, /GH_AW_SAFE_OUTPUTS_CONFIG:/); assert.match(generated, /PREVIEW_ONLY: \$\{\{ \(env\.GH_AW_SAFE_OUTPUT_MODE == 'live' \|\| env\.GH_AW_SAFE_OUTPUT_MODE == 'review'\) && 'false' \|\| 'true' \}\}/); } @@ -633,14 +643,15 @@ test("clean-room compilation emits the expected GitHub Actions settings", { time test("Pages is an explicit least-privilege add-on", () => { const rootManifest = readFileSync(join(root, "aw.yml"), "utf8"); - const pagesManifest = readFileSync(join(root, "pages", "aw.yml"), "utf8"); const pagesWorkflow = readFileSync(join(root, "pages", "pages.yml"), "utf8"); + const reportAssets = ["aic-usage.mjs", "deployed-workflows.mjs", "inventory.mjs", "report.mjs"]; - assert.doesNotMatch(rootManifest, /pages\/pages|github-pages-report/); - assert.match(pagesManifest, /source: pages\.yml/); - assert.doesNotMatch(pagesManifest, /source: pages\/pages\.yml/); - assert.match(pagesManifest, /destination: \.github\/workflows\/pages\.yml/); - assert.match(pagesManifest, /\.github\/skills\/github-pages-report/); + assert.doesNotMatch(rootManifest, /pages\/pages|pages-report/); + assert.ok(!existsSync(join(root, "pages", "aw.yml")), "Pages must not masquerade as an Agentic Workflow package"); assert.match(pagesWorkflow, /pages: write/); assert.match(pagesWorkflow, /id-token: write/); + for (const assetName of reportAssets) { + assert.ok(existsSync(join(root, ".github", "scripts", "pages-report", assetName)), `missing report script ${assetName}`); + assert.match(pagesWorkflow, new RegExp(`\\.github/scripts/pages-report/${assetName.replace(".", "\\.")}`)); + } }); \ No newline at end of file From ee5fdcd88c72659d5f7f3eca03a5723375281f1d Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer <mnkiefer@github.com> Date: Wed, 26 Aug 2026 09:17:12 +0200 Subject: [PATCH 2/2] fix: update allowed repositories configuration in control precompute --- tests/helpers/control-precompute.mjs | 1 + tests/load/control-plane-load.test.mjs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/control-precompute.mjs b/tests/helpers/control-precompute.mjs index 6d3b60a..a075869 100644 --- a/tests/helpers/control-precompute.mjs +++ b/tests/helpers/control-precompute.mjs @@ -43,6 +43,7 @@ export function controlEnvironment(overrides = {}) { BATCH_SIZE: "100000", BATCH_INDEX: "0", ALLOWED_OWNERS: "acme", + ALLOWED_REPOS: "", DISPATCH_MAX: "1", ROLLOUT_PERCENT: "100", SAFE_OUTPUT_MODE: "staged", diff --git a/tests/load/control-plane-load.test.mjs b/tests/load/control-plane-load.test.mjs index 4c6bce4..9508f1a 100644 --- a/tests/load/control-plane-load.test.mjs +++ b/tests/load/control-plane-load.test.mjs @@ -54,7 +54,6 @@ function runPrecompute(overrides = {}) { TARGET_REPO: "", MAX_REPOS: "1000", MAX_SCAN_REPOS: "100000", - ALLOWED_REPOS: "", DISPATCH_MAX: "1000", ROLLOUT_PERCENT: "10", WORKER_CREDITS_PER_TARGET: "0",