Skip to content

Commit 531ea51

Browse files
committed
feat: Process Issue Templates for DevIndex Opt-In (#9237)
1 parent 8a42789 commit 531ea51

1 file changed

Lines changed: 177 additions & 5 deletions

File tree

apps/devindex/services/OptIn.mjs

Lines changed: 177 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,29 @@ class OptIn extends Base {
112112
cursor = stargazers.pageInfo.endCursor;
113113
}
114114

115-
const uniqueLogins = [...new Set(optedInLogins)];
115+
// 2. Check Issues
116+
const issueResults = await this.processIssues();
117+
118+
let uniqueLogins = [...new Set(optedInLogins)];
119+
let loginsToReAdd = [...uniqueLogins]; // Stars can reverse blocklist
120+
let issuesToClose = [];
121+
122+
if (issueResults) {
123+
uniqueLogins.push(...issueResults.selfLogins);
124+
uniqueLogins.push(...issueResults.othersLogins);
125+
loginsToReAdd.push(...issueResults.selfLogins); // ONLY self issues reverse blocklist
126+
issuesToClose.push(...issueResults.issuesToClose);
127+
128+
uniqueLogins = [...new Set(uniqueLogins)];
129+
loginsToReAdd = [...new Set(loginsToReAdd)];
130+
}
116131

117132
if (uniqueLogins.length > 0) {
118133
console.log(`[OptIn] Found ${uniqueLogins.length} new opt-in requests:`, uniqueLogins);
119134

120-
// 1. Remove from blocklist if they are there
135+
// 1. Remove from blocklist if they are there (ONLY stargazers and self-issues)
121136
const blocklist = await Storage.getBlocklist();
122-
const blockedUsersToReAdd = uniqueLogins.filter(login => blocklist.has(login.toLowerCase()));
137+
const blockedUsersToReAdd = loginsToReAdd.filter(login => blocklist.has(login.toLowerCase()));
123138

124139
if (blockedUsersToReAdd.length > 0) {
125140
console.log(`[OptIn] Removing from blocklist:`, blockedUsersToReAdd);
@@ -133,24 +148,181 @@ class OptIn extends Base {
133148
const tracker = await Storage.getTracker();
134149
const existingTracker = new Set(tracker.map(t => t.login.toLowerCase()));
135150

151+
// We must also ensure we don't add "othersLogins" that are on the blocklist,
152+
// since we didn't remove them above.
153+
const currentBlocklist = await Storage.getBlocklist();
154+
136155
const toAdd = uniqueLogins.filter(login => {
137156
const lLogin = login.toLowerCase();
138-
return !existingUsers.has(lLogin) && !existingTracker.has(lLogin);
157+
return !existingUsers.has(lLogin) && !existingTracker.has(lLogin) && !currentBlocklist.has(lLogin);
139158
});
140159

141160
if (toAdd.length > 0) {
142161
console.log(`[OptIn] Adding to tracker:`, toAdd);
143162
const trackerUpdates = toAdd.map(login => ({ login, lastUpdate: null }));
144163
await Storage.updateTracker(trackerUpdates);
145164
} else {
146-
console.log(`[OptIn] All opted-in users are already tracked or indexed.`);
165+
console.log(`[OptIn] All opted-in users are already tracked, indexed, or blocked.`);
147166
}
148167

149168
await Storage.saveOptInSync({ lastCheck: newLastCheck });
150169
console.log(`[OptIn] Processed opt-ins and updated sync state to ${newLastCheck}.`);
151170
} else {
152171
console.log('[OptIn] No new opt-in requests found.');
153172
}
173+
174+
// 3. Close Issues and Leave Comment
175+
if (issuesToClose.length > 0) {
176+
await this.closeIssues(issuesToClose);
177+
}
178+
}
179+
180+
async processIssues() {
181+
console.log('[OptIn] Checking for new opt-in issue requests...');
182+
let hasNextPage = true;
183+
let cursor = null;
184+
let selfLogins = [];
185+
let othersLogins = [];
186+
let issuesToClose = [];
187+
188+
while (hasNextPage) {
189+
const query = `
190+
query($owner: String!, $name: String!, $cursor: String) {
191+
repository(owner: $owner, name: $name) {
192+
issues(first: 100, states: OPEN, labels: ["devindex-opt-in"], after: $cursor) {
193+
pageInfo {
194+
hasNextPage
195+
endCursor
196+
}
197+
nodes {
198+
id
199+
number
200+
title
201+
body
202+
author {
203+
login
204+
}
205+
}
206+
}
207+
}
208+
}`;
209+
210+
const variables = {
211+
owner: this.optInRepoOwner,
212+
name: this.optInRepoName,
213+
cursor: cursor
214+
};
215+
216+
let data;
217+
try {
218+
data = await GitHub.query(query, variables, 3, 'OptIn Issues');
219+
} catch (err) {
220+
if (err.message.includes('NOT_FOUND') || err.message.includes('Could not resolve')) {
221+
return null;
222+
}
223+
throw err;
224+
}
225+
226+
const issues = data?.repository?.issues;
227+
if (!issues) break;
228+
229+
const nodes = issues.nodes || [];
230+
231+
for (const issue of nodes) {
232+
if (issue.title.includes('Opt-In Request:')) {
233+
if (issue.author && issue.author.login) {
234+
selfLogins.push(issue.author.login);
235+
issuesToClose.push({ id: issue.id, type: 'self', logins: [issue.author.login] });
236+
}
237+
} else if (issue.title.includes('Opt-In Nomination:')) {
238+
const match = issue.body.match(/### GitHub Usernames\s*([\s\S]*?)(?:###|$)/);
239+
if (match) {
240+
const text = match[1];
241+
const usernames = text.split('\n')
242+
.map(u => u.trim())
243+
.filter(u => u && !u.startsWith('-') && !u.startsWith('['));
244+
245+
const validUsernames = [];
246+
const invalidUsernames = [];
247+
248+
for (const uname of usernames) {
249+
try {
250+
await GitHub.rest(`users/${uname}`);
251+
validUsernames.push(uname);
252+
othersLogins.push(uname);
253+
} catch (e) {
254+
invalidUsernames.push(uname);
255+
}
256+
}
257+
258+
issuesToClose.push({
259+
id: issue.id,
260+
type: 'others',
261+
validLogins: validUsernames,
262+
invalidLogins: invalidUsernames
263+
});
264+
} else {
265+
issuesToClose.push({ id: issue.id, type: 'invalid' });
266+
}
267+
}
268+
}
269+
270+
hasNextPage = issues.pageInfo.hasNextPage;
271+
cursor = issues.pageInfo.endCursor;
272+
}
273+
274+
return { selfLogins, othersLogins, issuesToClose };
275+
}
276+
277+
async closeIssues(issues) {
278+
for (const issue of issues) {
279+
try {
280+
let commentBody = "";
281+
if (issue.type === 'self') {
282+
commentBody = `Thank you for opting in! @${issue.logins[0]} has been added to our tracking queue.\n\n*This issue has been automatically closed.*`;
283+
} else if (issue.type === 'others') {
284+
if (issue.validLogins && issue.validLogins.length > 0) {
285+
commentBody = `Thank you for your nominations!\n\n`;
286+
commentBody += `**Successfully Added:**\n${issue.validLogins.map(u => `- @${u}`).join('\n')}\n\n`;
287+
if (issue.invalidLogins && issue.invalidLogins.length > 0) {
288+
commentBody += `**Failed Validation (Not Found):**\n${issue.invalidLogins.map(u => `- ${u}`).join('\n')}\n\n`;
289+
}
290+
} else if (issue.invalidLogins && issue.invalidLogins.length > 0) {
291+
commentBody = `We could not validate any of the provided usernames. Please double-check them and submit a new request if needed.\n\n`;
292+
commentBody += `**Failed Validation (Not Found):**\n${issue.invalidLogins.map(u => `- ${u}`).join('\n')}\n\n`;
293+
} else {
294+
commentBody = `We could not parse any usernames from this issue. Please ensure you follow the issue template format.\n\n`;
295+
}
296+
commentBody += `*This issue has been automatically closed.*`;
297+
} else {
298+
commentBody = `We could not parse the usernames from this issue. Please ensure you follow the issue template format.\n\n*This issue has been automatically closed.*`;
299+
}
300+
301+
// Add Comment
302+
const commentQuery = `
303+
mutation($subjectId: ID!, $body: String!) {
304+
addComment(input: {subjectId: $subjectId, body: $body}) {
305+
clientMutationId
306+
}
307+
}`;
308+
await GitHub.query(commentQuery, {
309+
subjectId: issue.id,
310+
body: commentBody
311+
}, 3, `OptIn Comment ${issue.id}`);
312+
313+
// Close Issue
314+
const closeQuery = `
315+
mutation($issueId: ID!) {
316+
closeIssue(input: {issueId: $issueId}) {
317+
clientMutationId
318+
}
319+
}`;
320+
await GitHub.query(closeQuery, { issueId: issue.id }, 3, `OptIn Close ${issue.id}`);
321+
console.log(`[OptIn] Closed issue ${issue.id}`);
322+
} catch (err) {
323+
console.error(`[OptIn] Failed to close issue ${issue.id}:`, err.message);
324+
}
325+
}
154326
}
155327
}
156328

0 commit comments

Comments
 (0)