Files
awesome-mcp-servers/.github/workflows/check-glama.yml
2026-03-05 04:43:32 -05:00

355 lines
14 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Check Glama Link
on:
pull_request_target:
types: [opened, edited, synchronize, closed]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
# Post-merge welcome comment
welcome:
if: github.event.action == 'closed' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Post welcome comment
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr_number = context.payload.pull_request.number;
const marker = '<!-- welcome-comment -->';
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr_number,
per_page: 100,
});
if (!comments.some(c => c.body.includes(marker))) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: `${marker}\nThank you for your contribution! Your server has been merged.
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.
If you also have a remote server, you can list it under https://glama.ai/mcp/connectors`
});
}
# Validation checks (only on open PRs)
check-submission:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Validate PR submission
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const pr_number = context.payload.pull_request.number;
// Read existing README to check for duplicates
const readme = fs.readFileSync('README.md', 'utf8');
const existingUrls = new Set();
const urlRegex = /\(https:\/\/github\.com\/[^)]+\)/gi;
for (const match of readme.matchAll(urlRegex)) {
existingUrls.add(match[0].toLowerCase());
}
// Get the PR diff
const { data: files } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: pr_number,
per_page: 100,
});
// Permitted emojis
const permittedEmojis = [
'\u{1F396}\uFE0F', // 🎖️ official
'\u{1F40D}', // 🐍 Python
'\u{1F4C7}', // 📇 TypeScript/JS
'\u{1F3CE}\uFE0F', // 🏎️ Go
'\u{1F980}', // 🦀 Rust
'#\uFE0F\u20E3', // #️⃣ C#
'\u2615', // ☕ Java
'\u{1F30A}', // 🌊 C/C++
'\u{1F48E}', // 💎 Ruby
'\u2601\uFE0F', // ☁️ Cloud
'\u{1F3E0}', // 🏠 Local
'\u{1F4DF}', // 📟 Embedded
'\u{1F34E}', // 🍎 macOS
'\u{1FAA9}', // 🪟 Windows
'\u{1F427}', // 🐧 Linux
];
// Only check added lines (starting with +) for glama link
const hasGlama = files.some(file =>
file.patch && file.patch.split('\n')
.filter(line => line.startsWith('+'))
.some(line => line.includes('glama.ai/mcp/servers'))
);
// Check added lines for emoji usage
const addedLines = files
.filter(f => f.patch)
.flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
.filter(line => line.includes('](https://github.com/'));
let hasValidEmoji = false;
let hasInvalidEmoji = false;
const invalidLines = [];
const badNameLines = [];
const duplicateUrls = [];
const nonGithubUrls = [];
// Check for non-GitHub URLs in added entry lines (list items with markdown links)
const allAddedEntryLines = files
.filter(f => f.patch)
.flatMap(f => f.patch.split('\n').filter(line => line.startsWith('+')))
.map(line => line.replace(/^\+/, ''))
.filter(line => /^\s*-\s*\[/.test(line));
for (const line of allAddedEntryLines) {
// Extract the primary link URL (first markdown link)
const linkMatch = line.match(/\]\((https?:\/\/[^)]+)\)/);
if (linkMatch) {
const url = linkMatch[1];
if (!url.startsWith('https://github.com/')) {
nonGithubUrls.push(url);
}
}
}
// Check for duplicates
for (const line of addedLines) {
const ghMatch = line.match(/\(https:\/\/github\.com\/[^)]+\)/i);
if (ghMatch && existingUrls.has(ghMatch[0].toLowerCase())) {
duplicateUrls.push(ghMatch[0].replace(/[()]/g, ''));
}
}
for (const line of addedLines) {
const usedPermitted = permittedEmojis.filter(e => line.includes(e));
if (usedPermitted.length > 0) {
hasValidEmoji = true;
} else {
// Line with a GitHub link but no permitted emoji
invalidLines.push(line.replace(/^\+/, '').trim());
}
// Check for emojis that aren't in the permitted list
const emojiRegex = /\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
const allEmojis = line.match(emojiRegex) || [];
const unknownEmojis = allEmojis.filter(e => !permittedEmojis.some(p => p.includes(e) || e.includes(p)));
if (unknownEmojis.length > 0) {
hasInvalidEmoji = true;
}
// Check that the link text uses full owner/repo format
// Pattern: [link-text](https://github.com/owner/repo)
const entryRegex = /\[([^\]]+)\]\(https:\/\/github\.com\/([^/]+)\/([^/)]+)\)/;
const match = line.match(entryRegex);
if (match) {
const linkText = match[1];
const expectedName = `${match[2]}/${match[3]}`;
// Link text should contain owner/repo (case-insensitive)
if (!linkText.toLowerCase().includes('/')) {
badNameLines.push({ linkText, expectedName });
}
}
}
const emojiOk = addedLines.length === 0 || (hasValidEmoji && !hasInvalidEmoji && invalidLines.length === 0);
const nameOk = badNameLines.length === 0;
const noDuplicates = duplicateUrls.length === 0;
const allGithub = nonGithubUrls.length === 0;
// Apply glama labels
const glamaLabel = hasGlama ? 'has-glama' : 'missing-glama';
const glamaLabelRemove = hasGlama ? 'missing-glama' : 'has-glama';
// Apply emoji labels
const emojiLabel = emojiOk ? 'has-emoji' : 'missing-emoji';
const emojiLabelRemove = emojiOk ? 'missing-emoji' : 'has-emoji';
// Apply name labels
const nameLabel = nameOk ? 'valid-name' : 'invalid-name';
const nameLabelRemove = nameOk ? 'invalid-name' : 'valid-name';
const labelsToAdd = [glamaLabel, emojiLabel, nameLabel];
const labelsToRemove = [glamaLabelRemove, emojiLabelRemove, nameLabelRemove];
if (!noDuplicates) {
labelsToAdd.push('duplicate');
} else {
labelsToRemove.push('duplicate');
}
if (!allGithub) {
labelsToAdd.push('non-github-url');
} else {
labelsToRemove.push('non-github-url');
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: labelsToAdd,
});
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label,
});
} catch (e) {
// Label wasn't present, ignore
}
}
// Post comments for issues, avoiding duplicates
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: pr_number,
per_page: 100,
});
// Glama comment
if (!hasGlama) {
const marker = '<!-- glama-check -->';
if (!comments.some(c => c.body.includes(marker))) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: `${marker}\nHey,
To ensure that only working servers are listed, we're updating our listing requirements.
Please complete the following steps:
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 (including a successfully built Docker image and a release).
2. **Update your PR** by adding a \`[glama](https://glama.ai/mcp/servers/...)\` link immediately after the GitHub repository link, pointing to your server's Glama listing page.
If you need any assistance, feel free to ask questions here or on [Discord](https://glama.ai/discord).
P.S. If your server already has a hosted endpoint, you can also list it under https://glama.ai/mcp/connectors.`
});
}
}
// Emoji comment
if (!emojiOk && addedLines.length > 0) {
const marker = '<!-- emoji-check -->';
if (!comments.some(c => c.body.includes(marker))) {
const emojiList = [
'🎖️ official implementation',
'🐍 Python',
'📇 TypeScript / JavaScript',
'🏎️ Go',
'🦀 Rust',
'#️⃣ C#',
'☕ Java',
'🌊 C/C++',
'💎 Ruby',
'☁️ Cloud Service',
'🏠 Local Service',
'📟 Embedded Systems',
'🍎 macOS',
'🪟 Windows',
'🐧 Linux',
].map(e => `- ${e}`).join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
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.
**Permitted emojis:**
${emojiList}
Please update your PR to include the appropriate emoji(s). See existing entries for examples.`
});
}
}
// Duplicate comment
if (!noDuplicates) {
const marker = '<!-- duplicate-check -->';
if (!comments.some(c => c.body.includes(marker))) {
const dupes = duplicateUrls.map(u => `- ${u}`).join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: `${marker}\nThe following server(s) are already listed in the repository:
${dupes}
Please remove the duplicate entries from your PR.`
});
}
}
// Non-GitHub URL comment
if (!allGithub) {
const marker = '<!-- url-check -->';
if (!comments.some(c => c.body.includes(marker))) {
const urls = nonGithubUrls.map(u => `- ${u}`).join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: `${marker}\nWe only accept servers hosted on GitHub. The following URLs are not GitHub links:
${urls}
Please update your PR to use a \`https://github.com/...\` repository link.`
});
}
}
// Name format comment
if (!nameOk) {
const marker = '<!-- name-check -->';
if (!comments.some(c => c.body.includes(marker))) {
const examples = badNameLines.map(
b => `- \`${b.linkText}\` should be \`${b.expectedName}\``
).join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: `${marker}\nThe entry name must use the full \`owner/repo\` format (not just the repo name).
${examples}
For example: \`[user/mcp-server-example](https://github.com/user/mcp-server-example)\`
Please update your PR to use the full repository name.`
});
}
}