Skip to content

Commit

Permalink
Implement github action to add contributors
Browse files Browse the repository at this point in the history
  • Loading branch information
Christian Wooddell authored and Christian Wooddell committed Nov 6, 2023
1 parent 4c0dda4 commit 0e4a2e0
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 7 deletions.
10 changes: 3 additions & 7 deletions .github/ISSUE_TEMPLATE/4_badge.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
---
name: 🥇 Leo Contributor Badge
about: Claiming a Leo Contributor Badge
title: "[Badge - YOUR_GH_USERNAME]"
title: "Add YOUR_USERNAME to contributors"
labels: 'badge'
---

## 🥇 Leo Contributor Badge

<!--
Hi Aleo team! I'm claiming my contributor badge for completing a developer tutorial. 😀

Github Username: <YOUR_GITHUB_USERNAME>
Tutorial Repo: <PUSHED_GITHUB_REPO_URL>
Repo: <PUSHED_GITHUB_REPO_URL>
Requested badge: <TUTORIAL_OR_CONTENT>

<!--
For badge type, if you used `leo new` or `leo example` e.g., helloworld, token, lottery, tictactoe, then enter "Tutorial" as your badge type. If you created a unique Leo application not under those examples, enter "Content" instead.
-->

(Fill in the request here.)

357 changes: 357 additions & 0 deletions .github/scripts/add_contributor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
const { Octokit } = require("@octokit/rest");
const github = require("@actions/github");

const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});

async function processIssue() {

const context = github.context;
const issueTitle = context.payload.issue.title;

// Check if the title of the issue matches the expected format
if (!issueTitle.startsWith("Add ") || !issueTitle.endsWith(" to contributors")) {
console.log("This is not a request to add a contributor.");
return;
}

const { owner, repo } = context.repo;
const issueNumber = context.payload.issue.number;
const contributorName = issueTitle.slice(4, -16);
// Regex checks for both markdown and plain text links
const repoRegex = /Repo: (https?:\/\/github\.com\/[^\s\)]+|\[URL\]\((https:\/\/github\.com\/[^\s\)]+)\))/;


const issueOpener = context.payload.issue.user.login;

// Check if the contributor name in the title matches the username of the issue opener
if (contributorName !== issueOpener) {
let message = `Hey @${issueOpener}, please make sure you're requesting to add your own name in the issue title! 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log(`The contributor name "${contributorName}" does not match the issue opener's username "${issueOpener}"`);
return;
}

// Check if the issue body contains a link to a GitHub repository
const repoMatch = context.payload.issue.body.match(repoRegex);
if (!repoMatch) {
let message = `Hey @${contributorName}, you need to include a link to your Leo repo in the issue body! 😄`;
commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.error("No repo URL found in the issue body.");
return;
}

const repoURL = (repoMatch[1] || repoMatch[2]).replace(/\)$/,'');
const [repoName, ownerName] = repoURL.split('/').reverse();

// Check if the user has starred the Leo repo
let hasStarred = false;

// Note: This doesn't handle pagination. If they starred the repo more than 100 starred repos ago we will have an issue.
try {
const starredRepos = await octokit.activity.listReposStarredByUser({
username: issueOpener,
per_page: 100
});
hasStarred = starredRepos.data.some(repo => repo.full_name === 'AleoHQ/leo');

} catch (error) {
console.error(`An error occurred while checking starred status: ${error}`);
}

if (hasStarred) {
console.log(`${contributorName} has starred the repo`);
} else {
let message = `Hey @${contributorName}, you need to star the [Leo repo](https://github.com/AleoHQ/leo) to be added as a contributor! Go give it a 🌟!`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log(`${contributorName} has not starred the repo`)
return;
}

// Extract the requested badge from the issue body.
const badgeRegex = /Requested badge: (\w+)/;
const match = context.payload.issue.body.match(badgeRegex);

if (!match) {
let message = `Hey @${contributorName}, you need to specify the requested badge in the issue body! 😄`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log('Badge not specified in the issue body.');
return;
}

const badgeType = match[1].toLowerCase();
console.log(`Badge Type: ${badgeType}`);

const badgeMapping = {
audio: {emoji: '🔊', title: 'Audio'},
a11y: {emoji: '♿️', title: 'Accessibility'},
bug: {emoji: '🐛', title: 'Bug reports'},
blog: {emoji: '📝', title: 'Blogposts'},
business: {emoji: '💼', title: 'Business Development'},
code: {emoji: '💻', title: 'Code'},
content: {emoji: '🖋', title: 'Content'},
data: {emoji: '🔣', title: 'Data'},
doc: {emoji: '📖', title: 'Documentation'},
design: {emoji: '🎨', title: 'Design'},
example: {emoji: '💡', title: 'Examples'},
eventOrganizing: {emoji: '📋', title: 'Event Organizers'},
financial: {emoji: '💵', title: 'Financial Support'},
fundingFinding: {emoji: '🔍', title: 'Funding/Grant Finders'},
ideas: {emoji: '🤔', title: 'Ideas & Planning'},
infra: {emoji: '🚇', title: 'Infrastructure'},
maintenance: {emoji: '🚧', title: 'Maintenance'},
mentoring: {emoji: '🧑‍🏫', title: 'Mentoring'},
platform: {emoji: '📦', title: 'Packaging'},
plugin: {emoji: '🔌', title: 'Plugin/utility libraries'},
projectManagement: {emoji: '📆', title: 'Project Management'},
promotion: {emoji: '📣', title: 'Promotion'},
question: {emoji: '💬', title: 'Answering Questions'},
research: {emoji: '🔬', title: 'Research'},
review: {emoji: '👀', title: 'Reviewed Pull Requests'},
security: {emoji: '🛡️', title: 'Security'},
tool: {emoji: '🔧', title: 'Tools'},
translation: {emoji: '🌍', title: 'Translation'},
test: {emoji: '⚠️', title: 'Tests'},
tutorial: {emoji: '✅', title: 'Tutorials'},
talk: {emoji: '📢', title: 'Talks'},
userTesting: {emoji: '📓', title: 'User Testing'},
video: {emoji: '📹', title: 'Videos'}
};

// Check that they are author of the linked repo
if (ownerName !== contributorName) {
let message = `Hey @${contributorName}, you need to link to your own repo! 😄`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log(`The contributor "${contributorName}" does not own the repo "${repoName}"`);
return;
}

// Check if the repo contains a valid Leo application
console.log("repo name", repoName)
try {
await octokit.repos.getContent({
owner: ownerName,
repo: repoName,
path: 'src/main.leo',
});
console.log("repo name", repoName)
console.log(`The repository "${repoName}" under owner "${ownerName}" contains a valid Leo application.`);
} catch (error) {
console.log("repo name", repoName)
let message = `Hey @${contributorName}, the repo you linked does not contain a valid Leo application! 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log(`The repository "${repoName}" under owner "${ownerName}" does not contain a valid Leo application.`);
return;
}

// Fetch README from the GitHub repo
const { data: readme } = await octokit.repos.getContent({
owner,
repo,
path: 'README.md',
});

const readmeContent = Buffer.from(readme.content, 'base64').toString('utf-8');

async function commentAndTagUser(owner, repo, issueNumber, contributorName, message) {
try {
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message,
});
console.log("Comment added successfully");
} catch (error) {
console.error("Error creating comment:", error);
}
}

async function createPRWithBadge({ owner, repo, updatedReadme, readme, contributorName, badgeType, issueNumber }) {
// 1. Create a new branch
const { data: branch } = await octokit.repos.getBranch({
owner,
repo,
branch: 'master'
});
const latestCommitSha = branch.commit.sha;
const branchName = `add-${badgeType}-badge-for-${contributorName}-${Date.now()}`; // Unique branch name
await octokit.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: latestCommitSha
});

// 2. Commit the changes to the new branch
const updatedReadmeBase64 = Buffer.from(updatedReadme).toString('base64');
await octokit.repos.createOrUpdateFileContents({
owner,
repo,
path: 'README.md',
message: `Add ${contributorName} to README contributors`,
content: updatedReadmeBase64,
sha: readme.sha,
branch: branchName
});

// 3. Open a Pull Request
const { data: createdPR } = await octokit.pulls.create({
owner,
repo,
title: `Add ${contributorName} to README contributors`,
head: branchName,
base: 'master',
body: `closes #${issueNumber}`
});

// 4. Add @AleoHQ/tech-ops as a reviewer
// TODO - change my name to '@AleoHQ/tech-ops'
await octokit.pulls.requestReviewers({
owner,
repo,
pull_number: createdPR.number,
reviewers: ['christianwooddell']
});

console.log(`Created a PR for "${contributorName}" with the "${badgeType}" badge.`);
}

// Check if the user's name exists in the README
const userRegex = new RegExp(contributorName);
if (userRegex.test(readmeContent)) {
console.log(`The contributor "${contributorName}" is found in the README.`);

const badgeCheckRegex = new RegExp(`<a href="https://github.com/${contributorName}/[^"]*" title=["“]${badgeType}(["”])?>`, 'i');

if (badgeCheckRegex.test(readmeContent)) {
// TODO: commentAndTagUser
let message = `Hey @${contributorName}, you already have the "${badgeType}" badge! 😄`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.log(`The contributor "${contributorName}" already has the "${badgeType}" badge.`);
return;
} else {
console.log('Badge not found in the README for the contributor.');
// Identify the position to insert the new badge for existing contributor
const insertionRegex = new RegExp(`(https://github.com/${contributorName}/[^"]*"[^>]*>.*?</a>)`);
const match = readmeContent.match(insertionRegex);

if (match) {
const insertionPoint = match.index + match[0].length;
const badgeDetails = badgeMapping[badgeType];

if (!badgeDetails) {
let message = `Hey @${contributorName}, the badge type "${badgeType}" is not recognized! 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
throw new Error(`Badge type "${badgeType}" not recognized.`);
}

// NOTE: Currently adds link to contributor's page if not tutorial or code
let badgeLink;
switch(badgeType.toLowerCase()) {
case 'tutorial':
badgeLink = `https://github.com/${contributorName}/${repoName}`;
break;
case 'code':
badgeLink = `https://github.com/AleoHQ/leo/commits?author=${contributorName}`;
break;
default:
badgeLink = `https://github.com/${contributorName}`;
break;
}

const newBadge = `<a href="${badgeLink}" title="${badgeDetails.title}">${badgeDetails.emoji}</a>`;

const updatedReadme = [
readmeContent.slice(0, insertionPoint),
newBadge,
readmeContent.slice(insertionPoint)
].join('');

await createPRWithBadge({
owner,
repo,
updatedReadme,
readme,
contributorName,
badgeType,
issueNumber
});

console.log(`Created a PR to add the "${badgeType}" badge for the contributor "${contributorName}" in the README.`);
} else {
let message = `Hey @${contributorName}, we had an issue with your request, please reach out 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.error(`Failed to find an insertion point for the "${badgeType}" badge for "${contributorName}".`);
}
}
} else {
console.log(`The contributor "${contributorName}" is NOT found in the README.`);
let contributorCountMatch = readmeContent.match(/Total count contributors: (\d+)/i);
let currentCount = contributorCountMatch ? parseInt(contributorCountMatch[1]) : 0;

const badgeDetails = badgeMapping[badgeType];
if (!badgeDetails) {
let message = `Hey @${contributorName}, we had an issue with your request, please reach out 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
throw new Error(`Badge type "${badgeType}" not recognized.`);
}

// regex that finds where to place the new badge by finding the second to last <tr> that contains <td>s
const trMatches = [...readmeContent.matchAll(/<tr>\s*([\s\S]*?)<\/tr>/g)];
const trMatchesContainingTds = trMatches.filter(trMatch => /<td[^>]*>[\s\S]*?<\/td>/g.test(trMatch[1]));
const secondToLastTrContainingTds = trMatchesContainingTds[trMatchesContainingTds.length - 2];

if (secondToLastTrContainingTds) {
const tdMatchesInTr = secondToLastTrContainingTds[1].match(/<td[^>]*>[\s\S]*?<\/td>/g) || [];
let updatedReadme;
const newContributorBlock = `
<td align="center" valign="top" width="14.28%">${newBadge}<img src="https://avatars.githubusercontent.com/${contributorName}?s=80&v=4?s=100" width="100px;" alt="${contributorName}"/><br /><sub><b>${contributorName}</b></sub></a><br /><a href="https://github.com/${contributorName}/YOUR_REPO_NAME" title="${badgeDetails.title}">${badgeDetails.emoji}</a></td>
`;

if (tdMatchesInTr.length < 7) {
// Insert the new contributor in the last row if there are less than 7 contributors in that row
const lastTdEndIndex = secondToLastTrContainingTds.index + secondToLastTrContainingTds[0].lastIndexOf('</td>') + 5;
updatedReadme = [
readmeContent.slice(0, lastTdEndIndex),
newContributorBlock,
readmeContent.slice(lastTdEndIndex)
].join('');
} else {
// Insert a new row with the new contributor if there are already 7 contributors in the last row
updatedReadme = [
readmeContent.slice(0, secondToLastTrContainingTds.index + secondToLastTrContainingTds[0].length),
'\n<tr>',
newContributorBlock,
'</tr>\n',
readmeContent.slice(secondToLastTrContainingTds.index + secondToLastTrContainingTds[0].length)
].join('');
}

// Increment and update contributor count
currentCount++;
updatedReadme = updatedReadme.replace(/Total count contributors: \d+/i, `Total count contributors: ${currentCount}`);

await createPRWithBadge({
owner,
repo,
updatedReadme,
readme,
contributorName,
badgeType,
issueNumber
});
console.log(`Created a PR to add "${contributorName}" to the README contributors with the "${badgeType}" badge.`);
} else {
let message = `Hey @${contributorName}, we had an issue with your request, please reach out 😅`;
await commentAndTagUser(owner, repo, issueNumber, contributorName, message);
console.error(`Failed to find the insertion point in the README.`);
}
}
}

processIssue().catch(error => {
console.error(error);
process.exit(1);
});
Loading

0 comments on commit 0e4a2e0

Please sign in to comment.