main
yml 394 lines 15.4 KB
Raw
1 name: Check Glama Link
2
3 on:
4 pull_request_target:
5 types: [opened, edited, synchronize, closed]
6
7 permissions:
8 contents: read
9 pull-requests: write
10 issues: write
11
12 jobs:
13 # Post-merge welcome comment
14 welcome:
15 if: github.event.action == 'closed' && github.event.pull_request.merged == true
16 runs-on: ubuntu-latest
17 steps:
18 - name: Post welcome comment
19 uses: actions/github-script@v7
20 with:
21 script: |
22 const { owner, repo } = context.repo;
23 const pr_number = context.payload.pull_request.number;
24 const marker = '<!-- welcome-comment -->';
25
26 const { data: comments } = await github.rest.issues.listComments({
27 owner,
28 repo,
29 issue_number: pr_number,
30 per_page: 100,
31 });
32
33 if (!comments.some(c => c.body.includes(marker))) {
34 await github.rest.issues.createComment({
35 owner,
36 repo,
37 issue_number: pr_number,
38 body: `${marker}\nThank you for your contribution! Your server has been merged.
39
40 Are you in the MCP [Discord](https://glama.ai/mcp/discord)? Let me know your Discord username and I will give you a **server-author** flair.
41
42 If you also have a remote server, you can list it under https://glama.ai/mcp/connectors`
43 });
44 }
45
46 # Validation checks (only on open PRs)
47 check-submission:
48 if: github.event.action != 'closed'
49 runs-on: ubuntu-latest
50 steps:
51 - name: Checkout base branch
52 uses: actions/checkout@v4
53 with:
54 ref: ${{ github.event.pull_request.base.ref }}
55
56 - name: Validate PR submission
57 uses: actions/github-script@v7
58 with:
59 script: |
60 const fs = require('fs');
61 const { owner, repo } = context.repo;
62 const pr_number = context.payload.pull_request.number;
63
64 // Read existing README to check for duplicates
65 const readme = fs.readFileSync('README.md', 'utf8');
66 const existingUrls = new Set();
67 const urlRegex = /\(https:\/\/github\.com\/[^)]+\)/gi;
68 for (const match of readme.matchAll(urlRegex)) {
69 existingUrls.add(match[0].toLowerCase());
70 }
71
72 // Get the PR diff
73 const { data: files } = await github.rest.pulls.listFiles({
74 owner,
75 repo,
76 pull_number: pr_number,
77 per_page: 100,
78 });
79
80 // Permitted emojis
81 const permittedEmojis = [
82 '\u{1F396}\uFE0F', // 🎖️ official
83 '\u{1F40D}', // 🐍 Python
84 '\u{1F4C7}', // 📇 TypeScript/JS
85 '\u{1F3CE}\uFE0F', // 🏎️ Go
86 '\u{1F980}', // 🦀 Rust
87 '#\uFE0F\u20E3', // #️⃣ C#
88 '\u2615', // ☕ Java
89 '\u{1F30A}', // 🌊 C/C++
90 '\u{1F48E}', // 💎 Ruby
91 '\u2601\uFE0F', // ☁️ Cloud
92 '\u{1F3E0}', // 🏠 Local
93 '\u{1F4DF}', // 📟 Embedded
94 '\u{1F34E}', // 🍎 macOS
95 '\u{1FA9F}', // 🪟 Windows
96 '\u{1F427}', // 🐧 Linux
97 ];
98
99 // All added lines with GitHub links (includes stale diff noise)
100 const addedLines = files
101 .filter(f => f.patch)
102 .flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
103 .filter(line => line.includes('](https://github.com/'));
104
105 // Filter to only genuinely new entries (not already in the base README)
106 const newAddedLines = addedLines.filter(line => {
107 const ghMatch = line.match(/\(https:\/\/github\.com\/[^)]+\)/i);
108 return !ghMatch || !existingUrls.has(ghMatch[0].toLowerCase());
109 });
110
111 // Only check new entries for glama link
112 const hasGlama = newAddedLines.some(line => line.includes('glama.ai/mcp/servers/') && line.includes('/badges/score.svg'));
113
114 let hasValidEmoji = false;
115 let hasInvalidEmoji = false;
116 const invalidLines = [];
117 const badNameLines = [];
118 const duplicateUrls = [];
119 const nonGithubUrls = [];
120
121 // Check for non-GitHub URLs in added entry lines (list items with markdown links)
122 const allAddedEntryLines = files
123 .filter(f => f.patch)
124 .flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
125 .map(line => line.replace(/^\+/, ''))
126 .filter(line => /^\s*-\s*\[/.test(line));
127
128 for (const line of allAddedEntryLines) {
129 // Extract the primary link URL (first markdown link)
130 const linkMatch = line.match(/\]\((https?:\/\/[^)]+)\)/);
131 if (linkMatch) {
132 const url = linkMatch[1];
133 if (!url.startsWith('https://github.com/')) {
134 nonGithubUrls.push(url);
135 }
136 }
137 }
138
139 // Check for duplicates
140 for (const line of addedLines) {
141 const ghMatch = line.match(/\(https:\/\/github\.com\/[^)]+\)/i);
142 if (ghMatch && existingUrls.has(ghMatch[0].toLowerCase())) {
143 duplicateUrls.push(ghMatch[0].replace(/[()]/g, ''));
144 }
145 }
146
147 for (const line of newAddedLines) {
148 const usedPermitted = permittedEmojis.filter(e => line.includes(e));
149 if (usedPermitted.length > 0) {
150 hasValidEmoji = true;
151 } else {
152 invalidLines.push(line.replace(/^\+/, '').trim());
153 }
154
155 const emojiRegex = /\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
156 const allEmojis = line.match(emojiRegex) || [];
157 const unknownEmojis = allEmojis.filter(e => !permittedEmojis.some(p => p.includes(e) || e.includes(p)));
158 if (unknownEmojis.length > 0) {
159 hasInvalidEmoji = true;
160 }
161
162 const entryRegex = /\[([^\]]+)\]\(https:\/\/github\.com\/([^/]+)\/([^/)]+)\)/;
163 const match = line.match(entryRegex);
164 if (match) {
165 const linkText = match[1];
166 const expectedName = `${match[2]}/${match[3]}`;
167 if (!linkText.toLowerCase().includes('/')) {
168 badNameLines.push({ linkText, expectedName });
169 }
170 }
171 }
172
173 const emojiOk = newAddedLines.length === 0 || (hasValidEmoji && !hasInvalidEmoji && invalidLines.length === 0);
174 const nameOk = badNameLines.length === 0;
175 const noDuplicates = duplicateUrls.length === 0;
176 const allGithub = nonGithubUrls.length === 0;
177
178 // Apply glama labels
179 const glamaLabel = hasGlama ? 'has-glama' : 'missing-glama';
180 const glamaLabelRemove = hasGlama ? 'missing-glama' : 'has-glama';
181
182 // Apply emoji labels
183 const emojiLabel = emojiOk ? 'has-emoji' : 'missing-emoji';
184 const emojiLabelRemove = emojiOk ? 'missing-emoji' : 'has-emoji';
185
186 // Apply name labels
187 const nameLabel = nameOk ? 'valid-name' : 'invalid-name';
188 const nameLabelRemove = nameOk ? 'invalid-name' : 'valid-name';
189
190 const labelsToAdd = [glamaLabel, emojiLabel, nameLabel];
191 const labelsToRemove = [glamaLabelRemove, emojiLabelRemove, nameLabelRemove];
192
193 if (!noDuplicates) {
194 labelsToAdd.push('duplicate');
195 } else {
196 labelsToRemove.push('duplicate');
197 }
198
199 if (!allGithub) {
200 labelsToAdd.push('non-github-url');
201 } else {
202 labelsToRemove.push('non-github-url');
203 }
204
205 await github.rest.issues.addLabels({
206 owner,
207 repo,
208 issue_number: pr_number,
209 labels: labelsToAdd,
210 });
211
212 for (const label of labelsToRemove) {
213 try {
214 await github.rest.issues.removeLabel({
215 owner,
216 repo,
217 issue_number: pr_number,
218 name: label,
219 });
220 } catch (e) {
221 // Label wasn't present, ignore
222 }
223 }
224
225 // Post comments for issues, avoiding duplicates
226 const { data: comments } = await github.rest.issues.listComments({
227 owner,
228 repo,
229 issue_number: pr_number,
230 per_page: 100,
231 });
232
233 // Glama missing comment
234 if (!hasGlama) {
235 const marker = '<!-- glama-check -->';
236 if (!comments.some(c => c.body.includes(marker))) {
237 await github.rest.issues.createComment({
238 owner,
239 repo,
240 issue_number: pr_number,
241 body: `${marker}\nHey,
242
243 To ensure that only working servers are listed, we're updating our listing requirements.
244
245 Please complete the following steps:
246
247 1. **Ensure your server is listed on Glama.** If it isn't already, submit it at https://glama.ai/mcp/servers and verify that it passes all checks (note: you must add Dockerfile directly to Glama. For checks to pass, we only need the server to start and respond to introspection requests).
248
249 2. **Update your PR** by adding a Glama score badge after the server description, using this format:
250
251 \`[![OWNER/REPO MCP server](https://glama.ai/mcp/servers/OWNER/REPO/badges/score.svg)](https://glama.ai/mcp/servers/OWNER/REPO)\`
252
253 Replace \`OWNER/REPO\` with your server's Glama path.
254
255 If you need any assistance, feel free to ask questions here or on [Discord](https://glama.ai/discord).
256
257 P.S. If your server already has a hosted endpoint, you can also list it under https://glama.ai/mcp/connectors.`
258 });
259 }
260 }
261
262 // Glama badge score comment — posted once the PR includes a glama link
263 if (hasGlama) {
264 const marker = '<!-- glama-badge-check -->';
265 if (!comments.some(c => c.body.includes(marker))) {
266 const glamaLines = files
267 .filter(f => f.patch)
268 .flatMap(f => f.patch.split('\n').filter(l => l.startsWith('+')))
269 .filter(l => l.includes('glama.ai/mcp/servers/') && l.includes('/badges/score.svg'))
270 .filter(l => {
271 const ghMatch = l.match(/\(https:\/\/github\.com\/[^)]+\)/i);
272 return !ghMatch || !existingUrls.has(ghMatch[0].toLowerCase());
273 });
274
275 let glamaServerPath = '';
276 for (const line of glamaLines) {
277 const glamaMatch = line.match(/glama\.ai\/mcp\/servers\/([^/)\s]+\/[^/)\s]+)/);
278 if (glamaMatch) {
279 glamaServerPath = glamaMatch[1];
280 break;
281 }
282 }
283
284 if (glamaServerPath) {
285 await github.rest.issues.createComment({
286 owner,
287 repo,
288 issue_number: pr_number,
289 body: `${marker}\nThank you for adding the Glama badge!
290
291 Please make sure the server has been evaluated by Glama and has a [quality score](https://glama.ai/mcp/servers/${glamaServerPath}/score):
292
293 [![${glamaServerPath} MCP server](https://glama.ai/mcp/servers/${glamaServerPath}/badges/score.svg)](https://glama.ai/mcp/servers/${glamaServerPath})
294
295 If you need any assistance, feel free to ask questions here or on [Discord](https://glama.ai/discord).`
296 });
297 }
298 }
299 }
300
301 // Emoji comment
302 if (!emojiOk && newAddedLines.length > 0) {
303 const marker = '<!-- emoji-check -->';
304 if (!comments.some(c => c.body.includes(marker))) {
305 const emojiList = [
306 '🎖️ – official implementation',
307 '🐍 – Python',
308 '📇 – TypeScript / JavaScript',
309 '🏎️ – Go',
310 '🦀 – Rust',
311 '#️⃣ – C#',
312 '☕ – Java',
313 '🌊 – C/C++',
314 '💎 – Ruby',
315 '☁️ – Cloud Service',
316 '🏠 – Local Service',
317 '📟 – Embedded Systems',
318 '🍎 – macOS',
319 '🪟 – Windows',
320 '🐧 – Linux',
321 ].map(e => `- ${e}`).join('\n');
322
323 await github.rest.issues.createComment({
324 owner,
325 repo,
326 issue_number: pr_number,
327 body: `${marker}\nYour submission is missing a required emoji tag or uses an unrecognized one. Each entry must include at least one of the permitted emojis after the repository link.
328
329 **Permitted emojis:**
330 ${emojiList}
331
332 Please update your PR to include the appropriate emoji(s). See existing entries for examples.`
333 });
334 }
335 }
336
337 // Duplicate comment
338 if (!noDuplicates) {
339 const marker = '<!-- duplicate-check -->';
340 if (!comments.some(c => c.body.includes(marker))) {
341 const dupes = duplicateUrls.map(u => `- ${u}`).join('\n');
342 await github.rest.issues.createComment({
343 owner,
344 repo,
345 issue_number: pr_number,
346 body: `${marker}\nThe following server(s) are already listed in the repository:
347
348 ${dupes}
349
350 Please remove the duplicate entries from your PR.`
351 });
352 }
353 }
354
355 // Non-GitHub URL comment
356 if (!allGithub) {
357 const marker = '<!-- url-check -->';
358 if (!comments.some(c => c.body.includes(marker))) {
359 const urls = nonGithubUrls.map(u => `- ${u}`).join('\n');
360 await github.rest.issues.createComment({
361 owner,
362 repo,
363 issue_number: pr_number,
364 body: `${marker}\nWe only accept servers hosted on GitHub. The following URLs are not GitHub links:
365
366 ${urls}
367
368 Please update your PR to use a \`https://github.com/...\` repository link.`
369 });
370 }
371 }
372
373 // Name format comment
374 if (!nameOk) {
375 const marker = '<!-- name-check -->';
376 if (!comments.some(c => c.body.includes(marker))) {
377 const examples = badNameLines.map(
378 b => `- \`${b.linkText}\` should be \`${b.expectedName}\``
379 ).join('\n');
380
381 await github.rest.issues.createComment({
382 owner,
383 repo,
384 issue_number: pr_number,
385 body: `${marker}\nThe entry name must use the full \`owner/repo\` format (not just the repo name).
386
387 ${examples}
388
389 For example: \`[user/mcp-server-example](https://github.com/user/mcp-server-example)\`
390
391 Please update your PR to use the full repository name.`
392 });
393 }
394 }