|
| 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