Skip to content

feat: add AgentNews social platform #66

feat: add AgentNews social platform

feat: add AgentNews social platform #66

name: Changed Services
on:
pull_request:
types: [opened, synchronize]
paths:
- "schemas/discovery.json"
- "schemas/services.ts"
- "public/services/llms.txt"
permissions:
pull-requests: write
jobs:
comment:
name: Report changed services
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Diff services and comment
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
// --- helpers ---
function git(cmd) {
return execSync(`git ${cmd}`, { encoding: 'utf8' });
}
function fileAt(ref, path) {
try { return git(`show ${ref}:${path}`); } catch { return null; }
}
function parseDiscovery(raw) {
if (!raw) return {};
const json = JSON.parse(raw);
const map = {};
for (const svc of json.services ?? []) {
map[svc.id] = svc;
}
return map;
}
function parseLlmsTxt(raw) {
if (!raw) return new Set();
// Extract the JSON array from the code block
const match = raw.match(/```json\n(\[.*?\])\n```/s);
if (!match) return new Set();
try {
const arr = JSON.parse(match[1]);
return new Set(arr.map(s => s.id));
} catch { return new Set(); }
}
function parseServicesTsIds(raw) {
if (!raw) return new Set();
const ids = new Set();
for (const m of raw.matchAll(/^\s*id:\s*["']([^"']+)["']/gm)) {
ids.add(m[1]);
}
return ids;
}
// --- main ---
const base = context.payload.pull_request.base.sha;
const head = 'HEAD';
const baseDiscovery = parseDiscovery(fileAt(base, 'schemas/discovery.json'));
const headDiscovery = parseDiscovery(fileAt(head, 'schemas/discovery.json'));
const baseLlms = parseLlmsTxt(fileAt(base, 'public/services/llms.txt'));
const headLlms = parseLlmsTxt(fileAt(head, 'public/services/llms.txt'));
const baseTs = parseServicesTsIds(fileAt(base, 'schemas/services.ts'));
const headTs = parseServicesTsIds(fileAt(head, 'schemas/services.ts'));
// Collect all service IDs across all three files
const allIds = new Set([
...Object.keys(baseDiscovery), ...Object.keys(headDiscovery),
...baseLlms, ...headLlms,
...baseTs, ...headTs,
]);
const added = [];
const removed = [];
const modified = [];
for (const id of [...allIds].sort()) {
const inBaseDisco = id in baseDiscovery;
const inHeadDisco = id in headDiscovery;
const inBaseTs = baseTs.has(id);
const inHeadTs = headTs.has(id);
const inBaseLlms = baseLlms.has(id);
const inHeadLlms = headLlms.has(id);
const wasAnywhere = inBaseDisco || inBaseTs || inBaseLlms;
const isAnywhere = inHeadDisco || inHeadTs || inHeadLlms;
if (!wasAnywhere && isAnywhere) {
added.push(id);
} else if (wasAnywhere && !isAnywhere) {
removed.push(id);
} else if (wasAnywhere && isAnywhere) {
// Check for endpoint-level changes via discovery.json
const baseSvc = baseDiscovery[id];
const headSvc = headDiscovery[id];
if (baseSvc && headSvc) {
const baseEndpoints = JSON.stringify(baseSvc.endpoints ?? []);
const headEndpoints = JSON.stringify(headSvc.endpoints ?? []);
const baseMeta = JSON.stringify({ ...baseSvc, endpoints: undefined });
const headMeta = JSON.stringify({ ...headSvc, endpoints: undefined });
if (baseEndpoints !== headEndpoints || baseMeta !== headMeta) {
modified.push(id);
}
}
}
}
if (added.length === 0 && removed.length === 0 && modified.length === 0) {
console.log('No service-level changes detected.');
return;
}
// --- build endpoint diffs ---
function endpointKey(ep) {
return `${ep.method} ${ep.path}`;
}
function endpointDiff(baseEps, headEps) {
const baseMap = new Map((baseEps ?? []).map(e => [endpointKey(e), e]));
const headMap = new Map((headEps ?? []).map(e => [endpointKey(e), e]));
const addedEps = [];
const removedEps = [];
const changedEps = [];
for (const [key, ep] of headMap) {
if (!baseMap.has(key)) {
addedEps.push(ep);
} else if (JSON.stringify(baseMap.get(key)) !== JSON.stringify(ep)) {
changedEps.push({ base: baseMap.get(key), head: ep });
}
}
for (const [key, ep] of baseMap) {
if (!headMap.has(key)) {
removedEps.push(ep);
}
}
return { addedEps, removedEps, changedEps };
}
function formatEndpointSection(id) {
const baseSvc = baseDiscovery[id];
const headSvc = headDiscovery[id];
const { addedEps, removedEps, changedEps } = endpointDiff(
baseSvc?.endpoints, headSvc?.endpoints
);
if (addedEps.length === 0 && removedEps.length === 0 && changedEps.length === 0) {
return '_No endpoint changes (metadata only)_';
}
const lines = [];
if (addedEps.length > 0) {
lines.push('**Added endpoints:**');
for (const ep of addedEps) {
const price = ep.payment?.amount
? ` — \`${ep.payment.amount}\``
: ep.payment?.dynamic ? ' — dynamic' : '';
lines.push(`- \`${ep.method} ${ep.path}\` ${ep.description}${price}`);
}
}
if (removedEps.length > 0) {
lines.push('**Removed endpoints:**');
for (const ep of removedEps) {
lines.push(`- ~~\`${ep.method} ${ep.path}\`~~ ${ep.description}`);
}
}
if (changedEps.length > 0) {
lines.push('**Modified endpoints:**');
for (const { base: b, head: h } of changedEps) {
const diffs = [];
if (b.description !== h.description) diffs.push(`desc: "${b.description}" → "${h.description}"`);
const bAmt = b.payment?.amount; const hAmt = h.payment?.amount;
if (bAmt !== hAmt) diffs.push(`amount: \`${bAmt ?? 'none'}\` → \`${hAmt ?? 'none'}\``);
if ((b.payment?.dynamic ?? false) !== (h.payment?.dynamic ?? false)) diffs.push(`dynamic: ${!!b.payment?.dynamic} → ${!!h.payment?.dynamic}`);
if ((b.payment?.intent ?? '') !== (h.payment?.intent ?? '')) diffs.push(`intent: ${b.payment?.intent ?? 'none'} → ${h.payment?.intent ?? 'none'}`);
if (b.docs !== h.docs) diffs.push(`docs changed`);
lines.push(`- \`${h.method} ${h.path}\` — ${diffs.join(', ') || 'updated'}`);
}
}
return lines.join('\n');
}
// --- build comment ---
const parts = ['## 🔀 Changed Services\n'];
// Which files were touched
const touchedFiles = [];
try {
const diff = git(`diff --name-only ${base} ${head}`);
for (const f of ['schemas/discovery.json', 'schemas/services.ts', 'public/services/llms.txt']) {
if (diff.includes(f)) touchedFiles.push(`\`${f}\``);
}
} catch {}
if (touchedFiles.length > 0) {
parts.push(`Files changed: ${touchedFiles.join(', ')}\n`);
}
if (added.length > 0) {
parts.push('### ✅ Added\n');
for (const id of added) {
const svc = headDiscovery[id];
const name = svc?.name ?? id;
const desc = svc?.description ? ` — ${svc.description}` : '';
parts.push(`#### ${name}${desc}\n`);
if (svc?.endpoints?.length > 0) {
parts.push('<details>');
parts.push(`<summary>Endpoints (${svc.endpoints.length})</summary>\n`);
for (const ep of svc.endpoints) {
const price = ep.payment?.amount
? ` — \`${ep.payment.amount}\``
: ep.payment?.dynamic ? ' — dynamic' : '';
parts.push(`- \`${ep.method} ${ep.path}\` ${ep.description}${price}`);
}
parts.push('\n</details>\n');
}
}
}
if (removed.length > 0) {
parts.push('### ❌ Removed\n');
for (const id of removed) {
const svc = baseDiscovery[id];
const name = svc?.name ?? id;
const desc = svc?.description ? ` — ${svc.description}` : '';
parts.push(`#### ~~${name}~~${desc}\n`);
if (svc?.endpoints?.length > 0) {
parts.push('<details>');
parts.push(`<summary>Endpoints (${svc.endpoints.length})</summary>\n`);
for (const ep of svc.endpoints) {
parts.push(`- ~~\`${ep.method} ${ep.path}\`~~ ${ep.description}`);
}
parts.push('\n</details>\n');
}
}
}
if (modified.length > 0) {
parts.push('### ✏️ Modified\n');
for (const id of modified) {
const svc = headDiscovery[id] ?? baseDiscovery[id];
const name = svc?.name ?? id;
parts.push(`#### ${name}\n`);
parts.push('<details>');
parts.push(`<summary>Endpoint changes</summary>\n`);
parts.push(formatEndpointSection(id));
parts.push('\n</details>\n');
}
}
const body = parts.join('\n');
// Upsert comment (find existing by marker)
const marker = '<!-- changed-services-bot -->';
const fullBody = `${marker}\n${body}`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
per_page: 100,
});
const existing = comments.find(c => c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: fullBody,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: fullBody,
});
}