feat: add AgentNews social platform #66
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 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, | |
| }); | |
| } |