Skip to content

Commit bc0dac1

Browse files
agent: deploy org-health
1 parent 27cac77 commit bc0dac1

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# .github/workflows/agent-org-health.yml
2+
# Self-improving org health agent
3+
# Monitors all 16 orgs: workflow health, repo counts, stale repos, security
4+
# Creates issues when things degrade, auto-fixes what it can
5+
6+
name: "Agent: Org Health Monitor"
7+
8+
on:
9+
schedule:
10+
- cron: '0 8 * * *' # Daily 8am UTC
11+
workflow_dispatch:
12+
inputs:
13+
action:
14+
description: 'Action to perform'
15+
required: false
16+
default: 'full_audit'
17+
type: choice
18+
options:
19+
- full_audit
20+
- fix_workflows
21+
- archive_stale
22+
- report_only
23+
24+
permissions:
25+
contents: write
26+
issues: write
27+
28+
jobs:
29+
health-check:
30+
name: "Org Health Audit"
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
35+
36+
- name: Audit all organizations
37+
id: audit
38+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
39+
with:
40+
script: |
41+
const ORGS = [
42+
'BlackRoad-OS-Inc', 'BlackRoad-OS', 'BlackRoad-AI', 'BlackRoad-Studio',
43+
'BlackRoad-Education', 'BlackRoad-Security', 'BlackRoad-Labs', 'BlackRoad-Hardware',
44+
'BlackRoad-Media', 'BlackRoad-Foundation', 'BlackRoad-Ventures', 'BlackRoad-Cloud',
45+
'BlackRoad-Gov', 'BlackRoad-Archive', 'BlackRoad-Interactive', 'Blackbox-Enterprises'
46+
];
47+
48+
const report = [];
49+
let totalRepos = 0, totalActive = 0, totalArchived = 0;
50+
let failingWorkflows = 0, healthyWorkflows = 0;
51+
let missingDescriptions = 0, missingTopics = 0;
52+
53+
for (const org of ORGS) {
54+
const orgReport = { org, repos: 0, active: 0, archived: 0, failing: 0, healthy: 0, issues: [] };
55+
56+
try {
57+
// Get all repos
58+
let page = 1;
59+
let repos = [];
60+
while (true) {
61+
const { data } = await github.rest.repos.listForOrg({ org, per_page: 100, page });
62+
if (data.length === 0) break;
63+
repos = repos.concat(data);
64+
page++;
65+
}
66+
67+
orgReport.repos = repos.length;
68+
orgReport.active = repos.filter(r => !r.archived).length;
69+
orgReport.archived = repos.filter(r => r.archived).length;
70+
71+
// Check for repos without descriptions
72+
const noDesc = repos.filter(r => !r.archived && (!r.description || r.description.length < 10));
73+
if (noDesc.length > 0) {
74+
orgReport.issues.push(`${noDesc.length} repos missing descriptions`);
75+
missingDescriptions += noDesc.length;
76+
}
77+
78+
// Check recent workflow failures (sample 5 repos)
79+
const activeRepos = repos.filter(r => !r.archived).slice(0, 5);
80+
for (const repo of activeRepos) {
81+
try {
82+
const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({
83+
owner: org, repo: repo.name, per_page: 5, status: 'failure'
84+
});
85+
if (runs.total_count > 0) {
86+
orgReport.failing++;
87+
failingWorkflows++;
88+
} else {
89+
orgReport.healthy++;
90+
healthyWorkflows++;
91+
}
92+
} catch { orgReport.healthy++; healthyWorkflows++; }
93+
}
94+
95+
// Check for very stale repos (no push in 90+ days)
96+
const now = new Date();
97+
const stale = repos.filter(r => {
98+
if (r.archived) return false;
99+
const pushed = new Date(r.pushed_at);
100+
return (now - pushed) / (1000 * 60 * 60 * 24) > 90;
101+
});
102+
if (stale.length > repos.length * 0.5) {
103+
orgReport.issues.push(`${stale.length}/${repos.length} repos stale (>90 days)`);
104+
}
105+
106+
} catch (e) {
107+
orgReport.issues.push(`API error: ${e.message}`);
108+
}
109+
110+
totalRepos += orgReport.repos;
111+
totalActive += orgReport.active;
112+
totalArchived += orgReport.archived;
113+
report.push(orgReport);
114+
}
115+
116+
// Generate summary
117+
const summary = `## BlackRoad Org Health Report — ${new Date().toISOString().split('T')[0]}
118+
119+
| Org | Repos | Active | Archived | Workflow Health | Issues |
120+
|-----|-------|--------|----------|---------------|--------|
121+
${report.map(r => `| ${r.org} | ${r.repos} | ${r.active} | ${r.archived} | ${r.failing > 0 ? '⚠️' : '✅'} ${r.healthy}/${r.healthy + r.failing} | ${r.issues.length > 0 ? r.issues.join('; ') : '—'} |`).join('\n')}
122+
123+
**Totals:** ${totalRepos} repos (${totalActive} active, ${totalArchived} archived)
124+
**Workflows:** ${healthyWorkflows} healthy, ${failingWorkflows} failing
125+
**Missing descriptions:** ${missingDescriptions}
126+
127+
---
128+
*Generated by BlackRoad Org Health Agent*`;
129+
130+
await core.summary.addRaw(summary).write();
131+
console.log(summary);
132+
133+
// Create issue if health is degrading
134+
const criticalIssues = report.filter(r => r.issues.length > 2);
135+
if (criticalIssues.length > 0) {
136+
try {
137+
await github.rest.issues.create({
138+
owner: 'BlackRoad-OS-Inc',
139+
repo: 'blackroad-operator',
140+
title: `[Agent] Org Health Alert — ${criticalIssues.length} orgs need attention`,
141+
body: summary,
142+
labels: ['agent', 'health', 'automated']
143+
});
144+
} catch (e) {
145+
console.log(`Could not create issue: ${e.message}`);
146+
}
147+
}
148+
149+
- name: Auto-fix missing descriptions
150+
if: inputs.action != 'report_only'
151+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
152+
with:
153+
script: |
154+
const ORGS = [
155+
'BlackRoad-OS-Inc', 'BlackRoad-OS', 'BlackRoad-AI', 'BlackRoad-Studio',
156+
'BlackRoad-Education', 'BlackRoad-Security', 'BlackRoad-Labs'
157+
];
158+
let fixed = 0;
159+
160+
for (const org of ORGS) {
161+
const { data: repos } = await github.rest.repos.listForOrg({ org, per_page: 100 });
162+
for (const repo of repos) {
163+
if (repo.archived || (repo.description && repo.description.length >= 10)) continue;
164+
if (fixed >= 30) break;
165+
166+
const name = repo.name.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
167+
const lang = repo.language ? `. Built with ${repo.language}` : '';
168+
const desc = `${name} — BlackRoad OS${lang}. Proprietary to BlackRoad OS, Inc.`;
169+
170+
try {
171+
await github.rest.repos.update({
172+
owner: org, repo: repo.name,
173+
description: desc.substring(0, 350)
174+
});
175+
fixed++;
176+
console.log(`Fixed: ${org}/${repo.name}`);
177+
} catch (e) {
178+
console.log(`Skip: ${org}/${repo.name} — ${e.message}`);
179+
}
180+
}
181+
}
182+
console.log(`Fixed ${fixed} descriptions`);

0 commit comments

Comments
 (0)