From 72a09e92fca65e78cf8b458c00a204af5b855fa3 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:34 +0100 Subject: [PATCH 01/35] Append info on author activity to Slack notification so we can decide about assignment requests more easily. --- scripts/contributor-issue-comment.js | 73 +++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 7b81c02..a0cfda8 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -38,7 +38,7 @@ module.exports = async ({ github, context, core }) => { }); labels = response.data.map(label => label.name); } catch (error) { - core.warning(`โš ๏ธ Failed to fetch labels on issue #${issueNumber}: ${error.message}`); + core.warning(`Failed to fetch labels on issue #${issueNumber}: ${error.message}`); labels = []; } return labels.some(label => label.toLowerCase() === name.toLowerCase()); @@ -54,7 +54,7 @@ module.exports = async ({ github, context, core }) => { }); return response.data.filter(comment => comment.user.login === username); } catch (error) { - core.warning(`โš ๏ธ Failed to fetch comments on issue #${issueNumber}: ${error.message}`); + core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); return []; } } @@ -80,13 +80,66 @@ module.exports = async ({ github, context, core }) => { return response; } + async function getIssues(assignee, state) { + try { + const response = await github.rest.issues.listForRepo({ + owner, + repo, + assignee, + state + }); + return response.data.filter(issue => !issue.pull_request); + } catch (error) { + core.warning(`Failed to fetch issues: ${error.message}`); + return []; + } + } + + async function getPullRequests(assignee, state) { + try { + const response = await github.rest.pulls.list({ + owner, + repo, + state + }); + return response.data.filter(pr => pr.user.login === assignee); + } catch (error) { + core.warning(`Failed to fetch pull requests: ${error.message}`); + return []; + } + } - if ( isCloseContributor || await hasLabel(ISSUE_LABEL_HELP_WANTED) ) { + // Format information about author's assigned open issues as '(Issues #1 #2 | PRs #3)' and PRs for Slack message + function formatAuthorActivity(issues, pullRequests) { + const parts = []; + + if (issues.length > 0) { + const issueLinks = issues.map(issue => `<${issue.html_url}|#${issue.number}>`).join(' '); + parts.push(`Issues ${issueLinks}`); + } else { + parts.push(`Issues none`); + } + + if (pullRequests.length > 0) { + const prLinks = pullRequests.map(pr => `<${pr.html_url}|#${pr.number}>`).join(' '); + parts.push(`PRs ${prLinks}`); + } else { + parts.push(`PRs none`); + } + + return `(${parts.join(' | ')})`; + } + + const sendNotificationToSupportDevChannel = isCloseContributor || await hasLabel(ISSUE_LABEL_HELP_WANTED); + + if (sendNotificationToSupportDevChannel) { core.setOutput('webhook_url', supportDevSlackWebhookUrl); } else { + // if we're not sending notification to #support-dev channel, + // send it to #support-dev-notifications and post GitHub bot reply if conditions are met core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); const matchedKeyword = keywordRegexes.find(regex => regex.test(commentBody)); - // post a bot reply if there is matched keyword and no previous bot comment in past hour + // post a bot reply only if there is matched keyword and no previous bot comment in past hour if(matchedKeyword){ let lastBotComment; let PastBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); @@ -103,9 +156,17 @@ module.exports = async ({ github, context, core }) => { } } - const message = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by ${commentAuthor}*`; - core.setOutput('slack_notification_comment', message); + let message = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by _${commentAuthor}_*`; + if (sendNotificationToSupportDevChannel) { + const [assignedOpenIssues, openPRs] = await Promise.all([ + getIssues(commentAuthor, 'open'), + getPullRequests(commentAuthor, 'open') + ]); + const authorActivity = formatAuthorActivity(assignedOpenIssues, openPRs); + message += ` _${authorActivity}_`; + } + core.setOutput('slack_notification_comment', message); } catch (error) { core.setFailed(`Action failed with error: ${error.message}`); } From 4696bc7b40fc4e1dc6f18d5f5ad30bfdfc16f48a Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:42 +0100 Subject: [PATCH 02/35] Improve naming + lint --- scripts/contributor-issue-comment.js | 112 +++++++++++++-------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index a0cfda8..5a29a9b 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -26,58 +26,56 @@ module.exports = async ({ github, context, core }) => { .filter(Boolean) .map(keyword => new RegExp(`\\b${keyword}\\b`, 'i')); - - async function hasLabel(name) { - let labels = []; - try { - const response = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: issueNumber - }); - labels = response.data.map(label => label.name); - } catch (error) { - core.warning(`Failed to fetch labels on issue #${issueNumber}: ${error.message}`); - labels = []; - } - return labels.some(label => label.toLowerCase() === name.toLowerCase()); + let labels = []; + try { + const response = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: issueNumber + }); + labels = response.data.map(label => label.name); + } catch (error) { + core.warning(`Failed to fetch labels on issue #${issueNumber}: ${error.message}`); + labels = []; + } + return labels.some(label => label.toLowerCase() === name.toLowerCase()); } async function findRecentCommentsByUser(username) { - try{ - let response = await github.rest.issues.listComments({ - owner, - repo, - issue_number: issueNumber, - since: oneHourBefore.toISOString() - }); - return response.data.filter(comment => comment.user.login === username); + try { + let response = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + since: oneHourBefore.toISOString() + }); + return response.data.filter(comment => comment.user.login === username); } catch (error) { - core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); - return []; + core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); + return []; } } - async function botReply(){ - let response = null; - try { - response = await github.rest.issues.createComment({ - owner, - repo, - issue_number: issueNumber, - body: BOT_MESSAGE_ISSUE_NOT_OPEN - }); - if (response?.data?.html_url) { - core.setOutput('bot_replied', true); - const slackMessage = `*[${repo}] <${response.data.html_url}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; - core.setOutput('slack_notification_bot_comment', slackMessage); - } - } catch (error) { - core.warning(`Failed to post bot comment: ${error.message}`); - core.setOutput('bot_replied', false); + async function botReply() { + let response = null; + try { + response = await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: BOT_MESSAGE_ISSUE_NOT_OPEN + }); + if (response?.data?.html_url) { + core.setOutput('bot_replied', true); + const slackMessage = `*[${repo}] <${response.data.html_url}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; + core.setOutput('slack_notification_bot_comment', slackMessage); } - return response; + } catch (error) { + core.warning(`Failed to post bot comment: ${error.message}`); + core.setOutput('bot_replied', false); + } + return response; } async function getIssues(assignee, state) { @@ -94,7 +92,7 @@ module.exports = async ({ github, context, core }) => { return []; } } - + async function getPullRequests(assignee, state) { try { const response = await github.rest.pulls.list({ @@ -109,7 +107,7 @@ module.exports = async ({ github, context, core }) => { } } - // Format information about author's assigned open issues as '(Issues #1 #2 | PRs #3)' and PRs for Slack message + // Format information about author's assigned open issues as '(Issues #1 #2 | PRs #3)' and PRs for Slack message function formatAuthorActivity(issues, pullRequests) { const parts = []; @@ -140,19 +138,17 @@ module.exports = async ({ github, context, core }) => { core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); const matchedKeyword = keywordRegexes.find(regex => regex.test(commentBody)); // post a bot reply only if there is matched keyword and no previous bot comment in past hour - if(matchedKeyword){ - let lastBotComment; - let PastBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); - if(PastBotComments.length > 0){ - lastBotComment = PastBotComments.at(-1); - core.setOutput('bot_replied', false); - core.setOutput('bot_reply_skipped', true); - const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${escapedTitle}> (less than 1 hour since last bot reply)*`; - core.setOutput('slack_notification_bot_skipped', slackMessage); - } else if(PastBotComments.length === 0){ - console.log("bot is replying"); - lastBotComment = await botReply(); - } + if (matchedKeyword) { + let recentBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); + if (recentBotComments.length > 0) { + core.setOutput('bot_replied', false); + core.setOutput('bot_reply_skipped', true); + const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${escapedTitle}> (less than 1 hour since last bot reply)*`; + core.setOutput('slack_notification_bot_skipped', slackMessage); + } else { + console.log("bot is replying"); + await botReply(); + } } } From afd99f5289ecbbb46165000ef355f5eb26d5ac92 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:45 +0100 Subject: [PATCH 03/35] Do not send support-dev notification when assignment request posted on an issue that is assigned to someone else, and send bot reply in such a case. --- scripts/constants.js | 3 + scripts/contributor-issue-comment.js | 112 ++++++++++++++++++--------- 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/scripts/constants.js b/scripts/constants.js index 6945880..23e4431 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -35,11 +35,14 @@ const ISSUE_LABEL_HELP_WANTED = 'help wanted'; const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; +const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; + module.exports = { LE_BOT_USERNAME, CLOSE_CONTRIBUTORS, KEYWORDS_DETECT_ASSIGNMENT_REQUEST, ISSUE_LABEL_HELP_WANTED, BOT_MESSAGE_ISSUE_NOT_OPEN, + BOT_MESSAGE_ALREADY_ASSIGNED, TEAMS_WITH_CLOSE_CONTRIBUTORS, }; diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 5a29a9b..701fbd4 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -2,7 +2,8 @@ const { LE_BOT_USERNAME, KEYWORDS_DETECT_ASSIGNMENT_REQUEST, ISSUE_LABEL_HELP_WANTED, - BOT_MESSAGE_ISSUE_NOT_OPEN + BOT_MESSAGE_ISSUE_NOT_OPEN, + BOT_MESSAGE_ALREADY_ASSIGNED } = require('./constants'); module.exports = async ({ github, context, core }) => { @@ -10,6 +11,7 @@ module.exports = async ({ github, context, core }) => { const issueNumber = context.payload.issue.number; const issueUrl = context.payload.issue.html_url; const issueTitle = context.payload.issue.title; + const issueAssignee = context.payload.issue.assignee?.login; const escapedTitle = issueTitle.replace(/"/g, '\\"'); const commentId = context.payload.comment.id; const commentTime = new Date(context.payload.comment.created_at); @@ -25,6 +27,9 @@ module.exports = async ({ github, context, core }) => { .map(k => k.trim().toLowerCase()) .filter(Boolean) .map(keyword => new RegExp(`\\b${keyword}\\b`, 'i')); + const isAssignmentRequest = keywordRegexes.find(regex => regex.test(commentBody)); + const isIssueAssignedToSomeoneElse = issueAssignee && (issueAssignee !== commentAuthor); + const isHelpWanted = await hasLabel(ISSUE_LABEL_HELP_WANTED); async function hasLabel(name) { let labels = []; @@ -57,25 +62,20 @@ module.exports = async ({ github, context, core }) => { } } - async function botReply() { + async function sendBotReply(message) { let response = null; try { response = await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, - body: BOT_MESSAGE_ISSUE_NOT_OPEN + body: message }); - if (response?.data?.html_url) { - core.setOutput('bot_replied', true); - const slackMessage = `*[${repo}] <${response.data.html_url}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; - core.setOutput('slack_notification_bot_comment', slackMessage); - } + return response?.data?.html_url; } catch (error) { core.warning(`Failed to post bot comment: ${error.message}`); - core.setOutput('bot_replied', false); + return null; } - return response; } async function getIssues(assignee, state) { @@ -92,7 +92,7 @@ module.exports = async ({ github, context, core }) => { return []; } } - + async function getPullRequests(assignee, state) { try { const response = await github.rest.pulls.list({ @@ -107,7 +107,8 @@ module.exports = async ({ github, context, core }) => { } } - // Format information about author's assigned open issues as '(Issues #1 #2 | PRs #3)' and PRs for Slack message + // Format information about author's assigned open issues + // as '(Issues #1 #2 | PRs #3)' and PRs for Slack message function formatAuthorActivity(issues, pullRequests) { const parts = []; @@ -128,41 +129,82 @@ module.exports = async ({ github, context, core }) => { return `(${parts.join(' | ')})`; } - const sendNotificationToSupportDevChannel = isCloseContributor || await hasLabel(ISSUE_LABEL_HELP_WANTED); + function shouldSendBotReply() { + if (isHelpWanted && isAssignmentRequest && isIssueAssignedToSomeoneElse) { + return [true, BOT_MESSAGE_ALREADY_ASSIGNED]; + } - if (sendNotificationToSupportDevChannel) { - core.setOutput('webhook_url', supportDevSlackWebhookUrl); - } else { - // if we're not sending notification to #support-dev channel, - // send it to #support-dev-notifications and post GitHub bot reply if conditions are met - core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); - const matchedKeyword = keywordRegexes.find(regex => regex.test(commentBody)); - // post a bot reply only if there is matched keyword and no previous bot comment in past hour - if (matchedKeyword) { - let recentBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); - if (recentBotComments.length > 0) { - core.setOutput('bot_replied', false); - core.setOutput('bot_reply_skipped', true); - const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${escapedTitle}> (less than 1 hour since last bot reply)*`; - core.setOutput('slack_notification_bot_skipped', slackMessage); - } else { - console.log("bot is replying"); - await botReply(); + if (!isHelpWanted && isAssignmentRequest) { + return [true, BOT_MESSAGE_ISSUE_NOT_OPEN]; + } + + return [false, null]; + } + + function shouldContactSupport() { + // for close contributors, send notification + // to #support-dev under all circumstances + if (isCloseContributor) { + return true; + } + + // for other contributors, do not send on non-help-wanted issues + if (!isHelpWanted) { + return false; + } + + // on help-wanted issues, do not send if it's an assignment + // request and issue is already assigned to someone else + if (isAssignmentRequest && isIssueAssignedToSomeoneElse) { + return false; + } + + return true; + } + + const [shouldPostBot, botMessage] = shouldSendBotReply(); + if (shouldPostBot) { + // post bot reply only when there are no previous + // bot comments in past hour to prevent overwhelming + // issue comment section + let recentBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); + if (recentBotComments.length > 0) { + const slackBotSkippedMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${escapedTitle}> (less than 1 hour since last bot reply)*`; + + core.setOutput('bot_reply_skipped', true); + core.setOutput('slack_notification_bot_skipped', slackBotSkippedMessage); + } else { + const botReplyUrl = await sendBotReply(botMessage); + if (botReplyUrl) { + const slackMessage = `*[${repo}] <${botReplyUrl}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; + core.setOutput('bot_replied', true); + core.setOutput('slack_notification_bot_comment', slackMessage); } } } - let message = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by _${commentAuthor}_*`; + const contactSupport = shouldContactSupport(); + let slackMessage = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by _${commentAuthor}_*`; - if (sendNotificationToSupportDevChannel) { + if (contactSupport) { + // Append activity info when sending to #support-dev + // to guide the decision on whether to assign an issue const [assignedOpenIssues, openPRs] = await Promise.all([ getIssues(commentAuthor, 'open'), getPullRequests(commentAuthor, 'open') ]); const authorActivity = formatAuthorActivity(assignedOpenIssues, openPRs); - message += ` _${authorActivity}_`; + slackMessage += ` _${authorActivity}_`; + + core.setOutput('webhook_url', supportDevSlackWebhookUrl); + core.setOutput('slack_notification_comment', slackMessage); + } else { + // if we're not sending notification to #support-dev, + // send it to #support-dev-notifications and if the comment + // appears to be an assignment request, post GitHub bot reply + core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); + core.setOutput('slack_notification_comment', slackMessage); } - core.setOutput('slack_notification_comment', message); } catch (error) { core.setFailed(`Action failed with error: ${error.message}`); } From 2c2fc3c445524a64ce515c104cfd2c7dee9279c7 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:48 +0100 Subject: [PATCH 04/35] Prevent bot replies on issues reported by the same user --- scripts/contributor-issue-comment.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 701fbd4..2917ef7 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -11,6 +11,7 @@ module.exports = async ({ github, context, core }) => { const issueNumber = context.payload.issue.number; const issueUrl = context.payload.issue.html_url; const issueTitle = context.payload.issue.title; + const issueCreator = context.payload.issue.user.login; const issueAssignee = context.payload.issue.assignee?.login; const escapedTitle = issueTitle.replace(/"/g, '\\"'); const commentId = context.payload.comment.id; @@ -111,25 +112,37 @@ module.exports = async ({ github, context, core }) => { // as '(Issues #1 #2 | PRs #3)' and PRs for Slack message function formatAuthorActivity(issues, pullRequests) { const parts = []; - + if (issues.length > 0) { const issueLinks = issues.map(issue => `<${issue.html_url}|#${issue.number}>`).join(' '); parts.push(`Issues ${issueLinks}`); } else { parts.push(`Issues none`); } - + if (pullRequests.length > 0) { const prLinks = pullRequests.map(pr => `<${pr.html_url}|#${pr.number}>`).join(' '); parts.push(`PRs ${prLinks}`); } else { parts.push(`PRs none`); } - + return `(${parts.join(' | ')})`; } function shouldSendBotReply() { + if (issueCreator === commentAuthor) { + // Strictly prevents all bot replies on issues reported + // by people using our apps - sometimes there's conversation + // in comments and in this context, it's strange to have the bot + // chime in if someone uses a keyword triggering the bot. + // (This means that a bot reply won't be sent if a contributor + // reports an issue and then requests its assignment. But that's + // acceptable trade-off, and we'd likely want to see the report anyway + // and then decide whether they can work on it). + return [false, null]; + } + if (isHelpWanted && isAssignmentRequest && isIssueAssignedToSomeoneElse) { return [true, BOT_MESSAGE_ALREADY_ASSIGNED]; } From 1964588151695723c0d923232fcc2434b1101b63 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:51 +0100 Subject: [PATCH 05/35] Add workflow documentation --- docs/contributor-issue-comment.md | 15 +++++++++++++++ scripts/contributor-issue-comment.js | 2 ++ 2 files changed, 17 insertions(+) create mode 100644 docs/contributor-issue-comment.md diff --git a/docs/contributor-issue-comment.md b/docs/contributor-issue-comment.md new file mode 100644 index 0000000..5f64e19 --- /dev/null +++ b/docs/contributor-issue-comment.md @@ -0,0 +1,15 @@ +# _Contributor issue comment_ workflow overview + +| Contributor type | Issue type | Comment type | #support-dev | #support-dev-notifications | GitHub bot | GitHub bot message | +|------------------|------------|--------------|--------------|---------------------------|------------------|-------------| +| **Core team** | Any | Any | No | No | No | - | +| **Close contributor** | Any | Any | **Yes** | No | No | - | +| **Issue creator** | `help-wanted` | Any | **Yes** | No | No | - | +| **Issue creator** | Private | Any | No | Yes | No | - | +| **Other** | Private | Regular | No | Yes | No | - | +| **Other** | Private | Assignment request | No | Yes | Yes`*` | `BOT_MESSAGE_ISSUE_NOT_OPEN` | +| **Other** | `help-wanted` | Regular | **Yes** | No | No | - | +| **Other** | Unassigned `help-wanted` | Assignment request | **Yes** | No | No | - | +| **Other** | `help-wanted` assigned to someone else | Assignment request | No | Yes | Yes`*` | `BOT_MESSAGE_ALREADY_ASSIGNED` | + +`*` There is an additional optimization that prevents more than one bot message per hour to not overwhelm issue comment section diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 2917ef7..51a655c 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -1,3 +1,5 @@ +// See docs/contributor-issue-comment.md + const { LE_BOT_USERNAME, KEYWORDS_DETECT_ASSIGNMENT_REQUEST, From d1a411a7d249ee6b920ce9ded6cc958f5654f4c3 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:55 +0100 Subject: [PATCH 06/35] Account for multiple assignees --- scripts/contributor-issue-comment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 51a655c..b7849e4 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -14,7 +14,7 @@ module.exports = async ({ github, context, core }) => { const issueUrl = context.payload.issue.html_url; const issueTitle = context.payload.issue.title; const issueCreator = context.payload.issue.user.login; - const issueAssignee = context.payload.issue.assignee?.login; + const issueAssignees = context.payload.issue.assignees?.map(assignee => assignee.login) || []; const escapedTitle = issueTitle.replace(/"/g, '\\"'); const commentId = context.payload.comment.id; const commentTime = new Date(context.payload.comment.created_at); @@ -31,7 +31,7 @@ module.exports = async ({ github, context, core }) => { .filter(Boolean) .map(keyword => new RegExp(`\\b${keyword}\\b`, 'i')); const isAssignmentRequest = keywordRegexes.find(regex => regex.test(commentBody)); - const isIssueAssignedToSomeoneElse = issueAssignee && (issueAssignee !== commentAuthor); + const isIssueAssignedToSomeoneElse = issueAssignees && issueAssignees.length > 0 && !issueAssignees.includes(commentAuthor); const isHelpWanted = await hasLabel(ISSUE_LABEL_HELP_WANTED); async function hasLabel(name) { From e3c92b59c5408c02d21e4f904964f13dcfd0a63d Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:27:59 +0100 Subject: [PATCH 07/35] Escape less and greater than characters Fixes broken Slack message formatting when < or > used in issue title. --- scripts/contributor-issue-comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index b7849e4..d447689 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -15,7 +15,7 @@ module.exports = async ({ github, context, core }) => { const issueTitle = context.payload.issue.title; const issueCreator = context.payload.issue.user.login; const issueAssignees = context.payload.issue.assignees?.map(assignee => assignee.login) || []; - const escapedTitle = issueTitle.replace(/"/g, '\\"'); + const escapedTitle = issueTitle.replace(/"/g, '\\"').replace(//g, '>'); const commentId = context.payload.comment.id; const commentTime = new Date(context.payload.comment.created_at); const oneHourBefore = new Date(commentTime - 3600000); From f7d1d34565a0354eec14b94befe59ae84fde7c8d Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:02 +0100 Subject: [PATCH 08/35] Run workflow only on open issues --- .github/workflows/contributor-issue-comment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index cbd4c2a..73c3a58 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -34,6 +34,7 @@ jobs: if: >- ${{ !github.event.issue.pull_request && + github.event.issue.state == 'open' && github.event.comment.author_association != 'OWNER' && github.event.comment.user.login != 'sentry-io[bot]' && github.event.comment.user.login != 'learning-equality-bot[bot]' && From 616ad439ce6b33767dc2213d45b193593ddda1bd Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:04 +0100 Subject: [PATCH 09/35] Re-organize logic that detects contributors --- .../workflows/contributor-issue-comment.yml | 34 +++--- ...ose-contributor.yml => is-contributor.yml} | 23 ++-- scripts/constants.js | 2 + scripts/contributor-issue-comment.js | 5 +- scripts/is-close-contributor.js | 56 ---------- scripts/is-contributor.js | 14 +++ scripts/utils.js | 103 ++++++++++++++++++ 7 files changed, 151 insertions(+), 86 deletions(-) rename .github/workflows/{is-close-contributor.yml => is-contributor.yml} (62%) delete mode 100644 scripts/is-close-contributor.js create mode 100644 scripts/is-contributor.js create mode 100644 scripts/utils.js diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index 73c3a58..eb31c93 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -4,11 +4,11 @@ on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" - required: true + description: "GitHub App ID for authentication" + required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" - required: true + description: "GitHub App Private Key for authentication" + required: true SLACK_WEBHOOK_URL: required: true description: "Webhook URL for Slack #support-dev channel" @@ -18,27 +18,24 @@ on: jobs: - check-if-close-contributor: - name: Check if user is a close contributor - uses: learningequality/.github/.github/workflows/is-close-contributor.yml@main + check-if-contributor: + name: Check if author is contributor + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: - LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} - LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} with: username: ${{ github.event.comment.user.login }} - + author_association: ${{ github.event.comment.author_association }} process-issue-comment: name: Process issue comment - needs: [check-if-close-contributor] + needs: [check-if-contributor] if: >- ${{ !github.event.issue.pull_request && github.event.issue.state == 'open' && - github.event.comment.author_association != 'OWNER' && - github.event.comment.user.login != 'sentry-io[bot]' && - github.event.comment.user.login != 'learning-equality-bot[bot]' && - (github.event.comment.author_association != 'MEMBER' || needs.check-if-close-contributor.outputs.is_close_contributor == 'true') + needs.check-if-contributor.outputs.is_contributor == 'true' }} runs-on: ubuntu-latest @@ -67,16 +64,15 @@ jobs: - name: Run script id: script - uses: actions/github-script@v7 + uses: actions/github-script@v7 with: github-token: ${{ steps.generate-token.outputs.token }} script: | - const script = require('./scripts/contributor-issue-comment.js'); - return await script({github, context, core}); + const script = require('./scripts/contributor-issue-comment.js'); + return await script({ github, context, core }); env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - IS_CLOSE_CONTRIBUTOR: ${{ needs.check-if-close-contributor.outputs.is_close_contributor }} - name: Send Slack notification about GitHub comment uses: slackapi/slack-github-action@v2.1.0 diff --git a/.github/workflows/is-close-contributor.yml b/.github/workflows/is-contributor.yml similarity index 62% rename from .github/workflows/is-close-contributor.yml rename to .github/workflows/is-contributor.yml index 91f502a..4885077 100644 --- a/.github/workflows/is-close-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -1,4 +1,4 @@ -name: Check if user is a close contributor +name: Check if user is contributor on: workflow_call: @@ -7,22 +7,26 @@ on: description: 'Github username' required: true type: string + author_association: + description: 'Author association from GitHub event' + required: true + type: string secrets: LE_BOT_APP_ID: required: true LE_BOT_PRIVATE_KEY: required: true outputs: - is_close_contributor: - description: "True if the user is a close contributor, otherwise false" - value: ${{ jobs.check-contributor.outputs.is_close_contributor }} + is_contributor: + description: "True if the user is a contributor (= not a core team member)" + value: ${{ jobs.check-contributor.outputs.is_contributor }} jobs: check-contributor: - name: Run check + name: Check if user is contributor runs-on: ubuntu-latest outputs: - is_close_contributor: ${{ steps.check-script.outputs.is_close_contributor }} + is_contributor: ${{ steps.run-script.outputs.is_contributor }} steps: - name: Generate App Token id: generate-token @@ -46,12 +50,13 @@ jobs: run: yarn install --frozen-lockfile - name: Run script - id: check-script + id: run-script uses: actions/github-script@v7 env: USERNAME: ${{ inputs.username }} + AUTHOR_ASSOCIATION: ${{ inputs.author_association }} with: github-token: ${{ steps.generate-token.outputs.token }} script: | - const script = require('./scripts/is-close-contributor.js'); - return await script({core, github, context }); + const script = require('./scripts/is-contributor.js'); + return await script({ core, github, context }); \ No newline at end of file diff --git a/scripts/constants.js b/scripts/constants.js index 23e4431..ecd0703 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -1,4 +1,5 @@ const LE_BOT_USERNAME = 'learning-equality-bot[bot]'; +const SENTRY_BOT_USERNAME = 'sentry-io[bot]'; // close contributors are treated a bit special in some workflows, // for example, we receive a high priority notification about their @@ -39,6 +40,7 @@ const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your inte module.exports = { LE_BOT_USERNAME, + SENTRY_BOT_USERNAME, CLOSE_CONTRIBUTORS, KEYWORDS_DETECT_ASSIGNMENT_REQUEST, ISSUE_LABEL_HELP_WANTED, diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index d447689..5651762 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -7,6 +7,7 @@ const { BOT_MESSAGE_ISSUE_NOT_OPEN, BOT_MESSAGE_ALREADY_ASSIGNED } = require('./constants'); +const { isCloseContributor } = require('./utils'); module.exports = async ({ github, context, core }) => { try { @@ -25,7 +26,6 @@ module.exports = async ({ github, context, core }) => { const owner = context.repo.owner; const supportDevSlackWebhookUrl = process.env.SLACK_WEBHOOK_URL; const supportDevNotificationsSlackWebhookUrl = process.env.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL; - const isCloseContributor = process.env.IS_CLOSE_CONTRIBUTOR === 'true'; const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST .map(k => k.trim().toLowerCase()) .filter(Boolean) @@ -33,6 +33,7 @@ module.exports = async ({ github, context, core }) => { const isAssignmentRequest = keywordRegexes.find(regex => regex.test(commentBody)); const isIssueAssignedToSomeoneElse = issueAssignees && issueAssignees.length > 0 && !issueAssignees.includes(commentAuthor); const isHelpWanted = await hasLabel(ISSUE_LABEL_HELP_WANTED); + const commentAuthorIsCloseContributor = await isCloseContributor(commentAuthor, { github, context, core }); async function hasLabel(name) { let labels = []; @@ -159,7 +160,7 @@ module.exports = async ({ github, context, core }) => { function shouldContactSupport() { // for close contributors, send notification // to #support-dev under all circumstances - if (isCloseContributor) { + if (commentAuthorIsCloseContributor) { return true; } diff --git a/scripts/is-close-contributor.js b/scripts/is-close-contributor.js deleted file mode 100644 index 18c0257..0000000 --- a/scripts/is-close-contributor.js +++ /dev/null @@ -1,56 +0,0 @@ -const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); - -module.exports = async ({ core, github, context }) => { - const username = process.env.USERNAME; - if (!username) { - core.setFailed('Missing username input.'); - return; - } - - if (CLOSE_CONTRIBUTORS.map(c => c.toLowerCase().trim()).includes(username.toLowerCase().trim())) { - core.info(`User '${username}' found in the CLOSE CONTRIBUTORS list.`); - core.setOutput('is_close_contributor', true); - return; - } - - const org = context.repo.owner; - - // Even though we check on team members here, it's best - // to add everyone to CLOSE_CONTRIBUTORS constant anyway - // for reliable results (e.g. check below won't work - // for private members) - const promises = TEAMS_WITH_CLOSE_CONTRIBUTORS.map(team_slug => - github.rest.teams.getMembershipForUserInOrg({ - org, - team_slug, - username, - }) - ); - - try { - const results = await Promise.allSettled(promises); - let isTeamMember = false; - - for (const result of results) { - if (result.status === 'fulfilled' && result.value.data.state === 'active') { - isTeamMember = true; - break; - } - - if (result.status === 'rejected' && result.reason.status !== 404) { - throw new Error(`API Error: ${result.reason.message}`); - } - } - - if (isTeamMember) { - core.info(`User '${username}' was found to be a member of a monitored team.`); - } else { - core.info(`User '${username}' was not found to be a member of a monitored team.`); - } - - core.setOutput('is_close_contributor', isTeamMember); - - } catch (error) { - core.setFailed(error.message); - } -}; \ No newline at end of file diff --git a/scripts/is-contributor.js b/scripts/is-contributor.js new file mode 100644 index 0000000..3bd9754 --- /dev/null +++ b/scripts/is-contributor.js @@ -0,0 +1,14 @@ +const { isContributor } = require('./utils'); + +/** + * Checks if a user is a contributor based on their + * username and author association. Sets `is_contributor` output. + */ +module.exports = async ({ core, github, context }) => { + const username = process.env.USERNAME; + const authorAssociation = process.env.AUTHOR_ASSOCIATION; + + const isUserContributor = await isContributor(username, authorAssociation, { github, context, core }); + + core.setOutput('is_contributor', isUserContributor); +}; diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..c397be3 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,103 @@ +const { LE_BOT_USERNAME, SENTRY_BOT_USERNAME } = require('./constants'); +const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); + +/** + * Checks if username belongs to one of our bots. + */ +async function isBot(username, { core }) { + if (!username) { + core.setFailed('Missing username'); + return false; + } + return [LE_BOT_USERNAME, SENTRY_BOT_USERNAME].includes(username); +} + +/** + * Checks if a user is a contributor (= not a core team member). + */ +async function isContributor(username, authorAssociation, { github, context, core }) { + if (!username) { + core.setFailed('Missing username'); + return false; + } + + if (!authorAssociation) { + core.setFailed('Missing authorAssociation'); + return false; + } + + if (authorAssociation === 'OWNER') { + return false; + } + + if (await isBot(username, { core })) { + return false; + } + + const isClose = await isCloseContributor(username, { github, context, core }); + // Some close contributors may be 'MEMBER's due to GSoC or other GitHub team + // memberships, so here we need to exclude only team members who are not + // close contributors + if (authorAssociation === 'MEMBER' && !isClose) { + return false; + } + + return true; +} + +/** + * Checks if a user is a close contributor by checking + * both the constants list and team membership in monitored teams. + */ +async function isCloseContributor(username, { github, context, core }) { + if (!username) { + core.setFailed('Missing username'); + return false; + } + + if (CLOSE_CONTRIBUTORS.map(c => c.toLowerCase().trim()).includes(username.toLowerCase().trim())) { + return true; + } + + const org = context.repo.owner; + + // Even though we check on team members here, it's best + // to add everyone to CLOSE_CONTRIBUTORS constant anyway + // for reliable results (e.g. check below won't work + // for people who have their membership set to private, + // and we don't have control over that) + const promises = TEAMS_WITH_CLOSE_CONTRIBUTORS.map(team_slug => + github.rest.teams.getMembershipForUserInOrg({ + org, + team_slug, + username, + }) + ); + + try { + const results = await Promise.allSettled(promises); + let isMember = false; + + for (const result of results) { + if (result.status === 'fulfilled' && result.value.data.state === 'active') { + isMember = true; + break; + } + + if (result.status === 'rejected' && result.reason.status !== 404) { + throw new Error(`API Error: ${result.reason.message}`); + } + } + + return isMember; + } catch (error) { + core.setFailed(error.message); + return false; + } +} + +module.exports = { + isContributor, + isCloseContributor, + isBot +}; From cf687ff4755de5d4e9a5e94029025de3e46a00dd Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:07 +0100 Subject: [PATCH 10/35] Use is-close-contributor in the workflow that updates community PRs spreadsheet. This also fixes problems with the spreadsheet not being updated when a PR was authored by a close contributor. --- .github/workflows/update-pr-spreadsheet.yml | 20 +++++++++++++++++++- scripts/update-pr-spreadsheet.js | 18 +----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index 0ec2f07..6d5b3cc 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -4,16 +4,34 @@ on: types: [assigned,unassigned,opened,closed,reopened] workflow_call: secrets: + LE_BOT_APP_ID: + description: "GitHub App ID for authentication" + required: true + LE_BOT_PRIVATE_KEY: + description: "GitHub App Private Key for authentication" + required: true CONTRIBUTIONS_SPREADSHEET_ID: required: true CONTRIBUTIONS_SHEET_NAME: required: true GH_UPLOADER_GCP_SA_CREDENTIALS: required: true + jobs: + check-if-contributor: + name: Check if author is contributor + uses: learningequality/.github/.github/workflows/is-contributor.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + with: + username: ${{ github.event.pull_request.user.login }} + author_association: ${{ github.event.pull_request.author_association }} + update-spreadsheet: + needs: [check-if-contributor] runs-on: ubuntu-latest - if: ${{ github.event.pull_request }} + if: ${{ needs.check-if-contributor.outputs.is_contributor == 'true' }} steps: # Checkout the code for the test-actions repository - name: Checkout code diff --git a/scripts/update-pr-spreadsheet.js b/scripts/update-pr-spreadsheet.js index 95fe226..73cec07 100644 --- a/scripts/update-pr-spreadsheet.js +++ b/scripts/update-pr-spreadsheet.js @@ -139,22 +139,6 @@ async function updateSpreadsheet(pullRequest) { } } -// Main function to handle pull request changes -async function handlePullRequestChange(pullRequest) { - // Filter out PRs from members of the organization - if ( - !pullRequest.user_site_admin && - pullRequest.user_type === "User" && - !pullRequest.author_association.includes("MEMBER") - ) { - await updateSpreadsheet(pullRequest); - } else { - console.log( - "PR skipped: Author is a member of the organization or a site admin." - ); - } -} - // Validate environment variables try { validateEnvVariables(); @@ -168,7 +152,7 @@ const githubEvent = JSON.parse(process.env.GITHUB_EVENT); // Extract PR data and run the script const prData = extractPRData(githubEvent); -handlePullRequestChange(prData).catch((error) => { +updateSpreadsheet(prData).catch((error) => { console.error("An error occurred:", error.message); process.exit(1); }); From f2928aab52b78d989d268eb02bfb26ca05d6d705 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:10 +0100 Subject: [PATCH 11/35] Add defensive check --- scripts/update-pr-spreadsheet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update-pr-spreadsheet.js b/scripts/update-pr-spreadsheet.js index 73cec07..eb62379 100644 --- a/scripts/update-pr-spreadsheet.js +++ b/scripts/update-pr-spreadsheet.js @@ -10,8 +10,8 @@ const extractPRData = (payload) => { title: pr.title, repo_name: pr.base.repo.name, created_at: pr.created_at, - requested_reviewers: pr.requested_reviewers.map(r => r.login).join(','), - assignees: pr.assignees.map(a => a.login).join(','), + requested_reviewers: pr.requested_reviewers?.map(r => r.login).join(',') || '', + assignees: pr.assignees?.map(a => a.login).join(',') || '', user_site_admin: pr.user.site_admin, user_type: pr.user.type, author_association: pr.author_association, From a445d678a84dc6410dc3e3f8e65a1b1d44b5ca6c Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:12 +0100 Subject: [PATCH 12/35] Update sheet workflow to follow conventions we settled on in majority of workflows (call-... file, github-script, same dependencies..) --- .../workflows/call-update-pr-spreadsheet.yml | 16 +++++ .github/workflows/update-pr-spreadsheet.yml | 65 +++++++++--------- scripts/update-pr-spreadsheet.js | 68 +++++++------------ 3 files changed, 73 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/call-update-pr-spreadsheet.yml diff --git a/.github/workflows/call-update-pr-spreadsheet.yml b/.github/workflows/call-update-pr-spreadsheet.yml new file mode 100644 index 0000000..f82a0a8 --- /dev/null +++ b/.github/workflows/call-update-pr-spreadsheet.yml @@ -0,0 +1,16 @@ +name: Update community pull requests spreadsheet + +on: + pull_request_target: + types: [assigned,unassigned,opened,closed,reopened] + +jobs: + call-workflow: + name: Call shared workflow + uses: learningequality/.github/.github/workflows/update-pr-spreadsheet.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + CONTRIBUTIONS_SPREADSHEET_ID: ${{ secrets.CONTRIBUTIONS_SPREADSHEET_ID }} + CONTRIBUTIONS_SHEET_NAME: ${{ secrets.CONTRIBUTIONS_SHEET_NAME }} + GH_UPLOADER_GCP_SA_CREDENTIALS: ${{ secrets.GH_UPLOADER_GCP_SA_CREDENTIALS }} diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index 6d5b3cc..e280342 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -1,7 +1,6 @@ name: Update community pull requests spreadsheet + on: - pull_request_target: - types: [assigned,unassigned,opened,closed,reopened] workflow_call: secrets: LE_BOT_APP_ID: @@ -33,33 +32,37 @@ jobs: runs-on: ubuntu-latest if: ${{ needs.check-if-contributor.outputs.is_contributor == 'true' }} steps: - # Checkout the code for the test-actions repository - - name: Checkout code - uses: actions/checkout@v4 - - # Checkout the .github repository - - name: Checkout .github repository - uses: actions/checkout@v4 - with: - repository: learningequality/.github - path: .github-repo - - # Setup Node.js - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: '20' - - # Install dependencies from the .github repository - - name: Install dependencies - run: npm install - working-directory: .github-repo + - name: Generate App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + - name: Checkout .github repository + uses: actions/checkout@v4 + with: + repository: learningequality/.github + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install --frozen-lockfile - # Run the update-pr-spreadsheet script - - name: Run script - run: node .github-repo/scripts/update-pr-spreadsheet.js - env: - SPREADSHEET_ID: ${{ secrets.CONTRIBUTIONS_SPREADSHEET_ID }} - SHEET_NAME: ${{ secrets.CONTRIBUTIONS_SHEET_NAME }} - GOOGLE_CREDENTIALS: ${{ secrets.GH_UPLOADER_GCP_SA_CREDENTIALS }} - GITHUB_EVENT: ${{ toJson(github.event) }} + - name: Run script + id: script + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const script = require('./scripts/update-pr-spreadsheet.js'); + return await script({ github, context, core }); + env: + CONTRIBUTIONS_SPREADSHEET_ID: ${{ secrets.CONTRIBUTIONS_SPREADSHEET_ID }} + CONTRIBUTIONS_SHEET_NAME: ${{ secrets.CONTRIBUTIONS_SHEET_NAME }} + GOOGLE_CREDENTIALS: ${{ secrets.GH_UPLOADER_GCP_SA_CREDENTIALS }} diff --git a/scripts/update-pr-spreadsheet.js b/scripts/update-pr-spreadsheet.js index eb62379..a637538 100644 --- a/scripts/update-pr-spreadsheet.js +++ b/scripts/update-pr-spreadsheet.js @@ -19,28 +19,10 @@ const extractPRData = (payload) => { }; }; -const SPREADSHEET_ID = process.env.SPREADSHEET_ID; -const SHEET_NAME = process.env.SHEET_NAME; -const GOOGLE_CREDENTIALS = process.env.GOOGLE_CREDENTIALS; - -// Validate environment variables -function validateEnvVariables() { - const missingVars = []; - if (!SPREADSHEET_ID) missingVars.push("SPREADSHEET_ID"); - if (!SHEET_NAME) missingVars.push("SHEET_NAME"); - if (!GOOGLE_CREDENTIALS) missingVars.push("GOOGLE_CREDENTIALS"); - - if (missingVars.length > 0) { - throw new Error( - `Missing required environment variables: ${missingVars.join(", ")}` - ); - } -} - // Set up GoogleAuth for Google Sheets API -async function authorize() { +async function authorize(googleCredentials) { try { - const credentials = JSON.parse(GOOGLE_CREDENTIALS); + const credentials = JSON.parse(googleCredentials); const auth = new google.auth.GoogleAuth({ credentials, scopes: ["https://www.googleapis.com/auth/spreadsheets"], @@ -49,14 +31,14 @@ async function authorize() { return google.sheets({ version: "v4", auth: authClient }); } catch (error) { throw new Error( - `Failed to authorize: ${error.message}. Please check your GOOGLE_CREDENTIALS.` + `Failed to authorize: ${error.message}.` ); } } // Update Google Sheets with pull request data -async function updateSpreadsheet(pullRequest) { - const sheets = await authorize(); +async function updateSpreadsheet(pullRequest, sheetId, sheetName, googleCredentials) { + const sheets = await authorize(googleCredentials); const prData = [ pullRequest.merged_at ? pullRequest.merged_at.split("T")[0].replace("'", "") : pullRequest.state === "closed" ? "closed" : pullRequest.state, pullRequest.html_url || "", @@ -73,8 +55,8 @@ async function updateSpreadsheet(pullRequest) { try { // Fetch existing rows from the sheet const { data } = await sheets.spreadsheets.values.get({ - spreadsheetId: SPREADSHEET_ID, - range: SHEET_NAME, + spreadsheetId: sheetId, + range: sheetName, }); const existingRows = data.values || []; @@ -102,7 +84,7 @@ async function updateSpreadsheet(pullRequest) { for (let col = 0; col < prData.length; col++) { if (existingData[col] !== prData[col]) { updates.push({ - range: `${SHEET_NAME}!${columns[col]}${rowToUpdate}`, + range: `${sheetName}!${columns[col]}${rowToUpdate}`, values: [[prData[col]]], }); } @@ -111,7 +93,7 @@ async function updateSpreadsheet(pullRequest) { if (updates.length > 0) { // Batch update changed columns await sheets.spreadsheets.values.batchUpdate({ - spreadsheetId: SPREADSHEET_ID, + spreadsheetId: sheetId, resource: { data: updates, valueInputOption: "RAW", @@ -127,8 +109,8 @@ async function updateSpreadsheet(pullRequest) { } else { // Append new row starting from column B await sheets.spreadsheets.values.append({ - spreadsheetId: SPREADSHEET_ID, - range: `${SHEET_NAME}!A:H`, + spreadsheetId: sheetId, + range: `${sheetName}!A:H`, valueInputOption: "RAW", resource: { values: [prData] }, }); @@ -139,20 +121,16 @@ async function updateSpreadsheet(pullRequest) { } } -// Validate environment variables -try { - validateEnvVariables(); -} catch (error) { - console.error(error.message); - process.exit(1); -} - -// Get the GitHub event data -const githubEvent = JSON.parse(process.env.GITHUB_EVENT); +module.exports = async ({ github, context, core }) => { + const sheetId = process.env.CONTRIBUTIONS_SPREADSHEET_ID; + const sheetName = process.env.CONTRIBUTIONS_SHEET_NAME; + const googleCredentials = process.env.GH_UPLOADER_GCP_SA_CREDENTIALS; -// Extract PR data and run the script -const prData = extractPRData(githubEvent); -updateSpreadsheet(prData).catch((error) => { - console.error("An error occurred:", error.message); - process.exit(1); -}); + try { + const prData = extractPRData(context.payload); + await updateSpreadsheet(prData, sheetId, sheetName, googleCredentials); + } catch (error) { + core.setFailed(`An error occurred: ${error.message}`); + throw error; + } +}; From 81dcea25aad3556023f8bad6d11edf4cbf2ebbdd Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:15 +0100 Subject: [PATCH 13/35] Fix sheet not being updated when.. when PR title changed, or when review requested. --- .github/workflows/call-update-pr-spreadsheet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/call-update-pr-spreadsheet.yml b/.github/workflows/call-update-pr-spreadsheet.yml index f82a0a8..bb275a0 100644 --- a/.github/workflows/call-update-pr-spreadsheet.yml +++ b/.github/workflows/call-update-pr-spreadsheet.yml @@ -2,7 +2,7 @@ name: Update community pull requests spreadsheet on: pull_request_target: - types: [assigned,unassigned,opened,closed,reopened] + types: [assigned,unassigned,opened,closed,reopened,edited,review_requested,review_request_removed] jobs: call-workflow: From 2e8de12f65b929074c3d673eabcb0c6d5491d64c Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:18 +0100 Subject: [PATCH 14/35] Add holiday message and PR reply workflows --- .../workflows/call-contributor-pr-reply.yml | 14 ++++ .github/workflows/call-holiday-message.yml | 16 ++++ .github/workflows/contributor-pr-reply.yml | 73 +++++++++++++++++ .github/workflows/holiday-message.yml | 82 +++++++++++++++++++ .github/workflows/is-holiday-active.yml | 52 ++++++++++++ ...ue-comment.md => community-automations.md} | 26 +++++- scripts/constants.js | 16 ++++ scripts/contributor-issue-comment.js | 70 ++++++---------- scripts/contributor-pr-reply.js | 23 ++++++ scripts/holiday-message.js | 60 ++++++++++++++ scripts/utils.js | 67 ++++++++++++++- 11 files changed, 449 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/call-contributor-pr-reply.yml create mode 100644 .github/workflows/call-holiday-message.yml create mode 100644 .github/workflows/contributor-pr-reply.yml create mode 100644 .github/workflows/holiday-message.yml create mode 100644 .github/workflows/is-holiday-active.yml rename docs/{contributor-issue-comment.md => community-automations.md} (55%) create mode 100644 scripts/contributor-pr-reply.js create mode 100644 scripts/holiday-message.js diff --git a/.github/workflows/call-contributor-pr-reply.yml b/.github/workflows/call-contributor-pr-reply.yml new file mode 100644 index 0000000..b3ab284 --- /dev/null +++ b/.github/workflows/call-contributor-pr-reply.yml @@ -0,0 +1,14 @@ +name: Send reply on a new contributor pull request + +on: + pull_request_target: + types: [opened] + +jobs: + call-workflow: + name: Call shared workflow + uses: learningequality/.github/.github/workflows/contributor-pr-reply.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} diff --git a/.github/workflows/call-holiday-message.yml b/.github/workflows/call-holiday-message.yml new file mode 100644 index 0000000..04d2ca8 --- /dev/null +++ b/.github/workflows/call-holiday-message.yml @@ -0,0 +1,16 @@ +name: Post holiday message on pull request or issue comment + +on: + pull_request_target: + types: [opened] + issue_comment: + types: [created] + +jobs: + call-workflow: + name: Call shared workflow + uses: learningequality/.github/.github/workflows/holiday-message.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml new file mode 100644 index 0000000..2f5dd17 --- /dev/null +++ b/.github/workflows/contributor-pr-reply.yml @@ -0,0 +1,73 @@ +name: Send reply on a new contributor pull request + +on: + workflow_call: + secrets: + LE_BOT_APP_ID: + description: "GitHub App ID for authentication" + required: true + LE_BOT_PRIVATE_KEY: + description: "GitHub App Private Key for authentication" + required: true + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: + required: true + description: "Webhook URL for Slack #support-dev-notifications channel" + +jobs: + check-if-contributor: + name: Check if author is contributor + uses: learningequality/.github/.github/workflows/is-contributor.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + with: + username: ${{ github.event.pull_request.user.login }} + author_association: ${{ github.event.pull_request.author_association }} + + send-reply: + name: Send reply + needs: [check-if-contributor] + if: ${{ needs.check-if-contributor.outputs.is_contributor == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Generate App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + - name: Checkout .github repository + uses: actions/checkout@v4 + with: + repository: learningequality/.github + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run script + id: script + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const script = require('./scripts/contributor-pr-reply.js'); + return await script({ github, context, core }); + + - name: Send Slack notification + if: ${{ steps.script.outputs.slack_notification != ''}} + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} + payload: > + { + "text": "${{ steps.script.outputs.slack_notification }}" + } diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml new file mode 100644 index 0000000..3ac6781 --- /dev/null +++ b/.github/workflows/holiday-message.yml @@ -0,0 +1,82 @@ +name: Post holiday message on pull request or issue comment + +on: + workflow_call: + secrets: + LE_BOT_APP_ID: + description: "GitHub App ID for authentication" + required: true + LE_BOT_PRIVATE_KEY: + description: "GitHub App Private Key for authentication" + required: true + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: + required: true + description: "Webhook URL for Slack #support-dev-notifications channel" + +jobs: + check-holiday-active: + name: Check if holiday message is active + uses: learningequality/.github/.github/workflows/is-holiday-active.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + check-if-contributor: + name: Check if author is contributor + needs: [check-holiday-active] + if: ${{ needs.check-holiday-active.outputs.is_holiday_active == 'true' }} + uses: learningequality/.github/.github/workflows/is-contributor.yml@main + secrets: + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + with: + username: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login || github.event_name == 'issue_comment' && github.event.comment.user.login }} + author_association: ${{ github.event_name == 'pull_request' && github.event.pull_request.author_association || github.event_name == 'issue_comment' && github.event.comment.author_association }} + + post-holiday-message: + name: Post holiday message + needs: [check-holiday-active, check-if-contributor] + if: ${{ needs.check-holiday-active.outputs.is_holiday_active == 'true' && needs.check-if-contributor.outputs.is_contributor == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Generate App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + - name: Checkout .github repository + uses: actions/checkout@v4 + with: + repository: learningequality/.github + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run script + id: script + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const script = require('./scripts/holiday-message.js'); + return await script({ github, context, core }); + + - name: Send Slack notification + if: ${{ steps.script.outputs.slack_notification != ''}} + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} + payload: > + { + "text": "${{ steps.script.outputs.slack_notification }}" + } diff --git a/.github/workflows/is-holiday-active.yml b/.github/workflows/is-holiday-active.yml new file mode 100644 index 0000000..3534d79 --- /dev/null +++ b/.github/workflows/is-holiday-active.yml @@ -0,0 +1,52 @@ +name: Check if holiday message is active + +on: + workflow_call: + secrets: + LE_BOT_APP_ID: + description: "GitHub App ID for authentication" + required: true + LE_BOT_PRIVATE_KEY: + description: "GitHub App Private Key for authentication" + required: true + outputs: + is_holiday_active: + description: "True if we're within the holiday message period" + value: ${{ jobs.check-holiday-active.outputs.is_holiday_active }} + +jobs: + check-holiday-active: + name: Check if holiday message is active + runs-on: ubuntu-latest + outputs: + is_holiday_active: ${{ steps.script.outputs.is_holiday_active }} + steps: + - name: Generate App Token + id: generate-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} + + - name: Checkout .github repository + uses: actions/checkout@v4 + with: + repository: learningequality/.github + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run script + id: script + uses: actions/github-script@v7 + with: + script: | + const { isHolidayMessageActive } = require('./scripts/utils'); + core.setOutput('is_holiday_active', isHolidayMessageActive()); diff --git a/docs/contributor-issue-comment.md b/docs/community-automations.md similarity index 55% rename from docs/contributor-issue-comment.md rename to docs/community-automations.md index 5f64e19..ebe4b23 100644 --- a/docs/contributor-issue-comment.md +++ b/docs/community-automations.md @@ -1,4 +1,6 @@ -# _Contributor issue comment_ workflow overview +# `contributor-issue-comment` + +Manages GitHub issue comments. Sends Slack notifications and GitHub bot replies. | Contributor type | Issue type | Comment type | #support-dev | #support-dev-notifications | GitHub bot | GitHub bot message | |------------------|------------|--------------|--------------|---------------------------|------------------|-------------| @@ -13,3 +15,25 @@ | **Other** | `help-wanted` assigned to someone else | Assignment request | No | Yes | Yes`*` | `BOT_MESSAGE_ALREADY_ASSIGNED` | `*` There is an additional optimization that prevents more than one bot message per hour to not overwhelm issue comment section + +In `scripts/contants.js` set: +- `BOT_MESSAGE_ISSUE_NOT_OPEN`: _Issue not open for contribution_ message text +- `BOT_MESSAGE_ALREADY_ASSIGNED`: _Issue already assigned_ message text + +# `contributor-pr-reply` + +Sends reply to a community pull requests. + +In `scripts/contants.js` set: +- `BOT_MESSAGE_PULL_REQUEST`: Message text + +# `holiday-message` + +Sends a holiday message to community pull requests and issue comments. + +In `scripts/contants.js` set: + +- `HOLIDAY_MESSAGE`: Message text +- `HOLIDAY_MESSAGE_START_DATE` and `HOLIDAY_MESSAGE_END_DATE`: From and till when the message should be sent + +Additionally before/after holidays, enable/disable all related workflows in all repositories that use it (search for `call-holiday-message`). diff --git a/scripts/constants.js b/scripts/constants.js index ecd0703..e9013ad 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -1,3 +1,5 @@ +// See docs/community-automations.md + const LE_BOT_USERNAME = 'learning-equality-bot[bot]'; const SENTRY_BOT_USERNAME = 'sentry-io[bot]'; @@ -38,6 +40,16 @@ const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your intere const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; +const BOT_MESSAGE_PULL_REQUEST = `Welcome! ๐Ÿ‘‹ \n\n We will assign a reviewerโ€”typically within next two weeks. In the meantime, to ensure a smooth review process, please **check that linting and tests pass, all issue requirements are satisfied, and that your contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai).** We'll be in touch! ๐Ÿ˜Š`; + +// Holiday message will be sent between the following datetimes +// Additionally before/after holidays, enable/disable all related workflows +// in all repositories that use it (search for `call-holiday-message`). +const HOLIDAY_MESSAGE_START_DATE = new Date('2025-12-15T00:00:00Z'); +const HOLIDAY_MESSAGE_END_DATE = new Date('2026-01-05T23:59:59Z'); + +const HOLIDAY_MESSAGE = `Seasonโ€™s greetings! ๐Ÿ‘‹ \n\n Weโ€™d like to thank everyone for another year of fruitful collaborations, engaging discussions, and for the continued support of our work. **Learning Equality will be on holidays from December 22 to January 5.** We look forward to much more in the new year and wish you a very happy holiday season!`; + module.exports = { LE_BOT_USERNAME, SENTRY_BOT_USERNAME, @@ -46,5 +58,9 @@ module.exports = { ISSUE_LABEL_HELP_WANTED, BOT_MESSAGE_ISSUE_NOT_OPEN, BOT_MESSAGE_ALREADY_ASSIGNED, + BOT_MESSAGE_PULL_REQUEST, TEAMS_WITH_CLOSE_CONTRIBUTORS, + HOLIDAY_MESSAGE_START_DATE, + HOLIDAY_MESSAGE_END_DATE, + HOLIDAY_MESSAGE }; diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 5651762..6149efc 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -1,4 +1,4 @@ -// See docs/contributor-issue-comment.md +// See docs/community-automations.md const { LE_BOT_USERNAME, @@ -7,19 +7,21 @@ const { BOT_MESSAGE_ISSUE_NOT_OPEN, BOT_MESSAGE_ALREADY_ASSIGNED } = require('./constants'); -const { isCloseContributor } = require('./utils'); +const { + isCloseContributor, + sendBotMessage, + escapeIssueTitleForSlackMessage, + hasRecentBotComment +} = require('./utils'); module.exports = async ({ github, context, core }) => { try { const issueNumber = context.payload.issue.number; const issueUrl = context.payload.issue.html_url; - const issueTitle = context.payload.issue.title; + const issueTitle = escapeIssueTitleForSlackMessage(context.payload.issue.title); const issueCreator = context.payload.issue.user.login; const issueAssignees = context.payload.issue.assignees?.map(assignee => assignee.login) || []; - const escapedTitle = issueTitle.replace(/"/g, '\\"').replace(//g, '>'); const commentId = context.payload.comment.id; - const commentTime = new Date(context.payload.comment.created_at); - const oneHourBefore = new Date(commentTime - 3600000); const commentAuthor = context.payload.comment.user.login; const commentBody = context.payload.comment.body; const repo = context.repo.repo; @@ -51,37 +53,6 @@ module.exports = async ({ github, context, core }) => { return labels.some(label => label.toLowerCase() === name.toLowerCase()); } - async function findRecentCommentsByUser(username) { - try { - let response = await github.rest.issues.listComments({ - owner, - repo, - issue_number: issueNumber, - since: oneHourBefore.toISOString() - }); - return response.data.filter(comment => comment.user.login === username); - } catch (error) { - core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); - return []; - } - } - - async function sendBotReply(message) { - let response = null; - try { - response = await github.rest.issues.createComment({ - owner, - repo, - issue_number: issueNumber, - body: message - }); - return response?.data?.html_url; - } catch (error) { - core.warning(`Failed to post bot comment: ${error.message}`); - return null; - } - } - async function getIssues(assignee, state) { try { const response = await github.rest.issues.listForRepo({ @@ -180,19 +151,24 @@ module.exports = async ({ github, context, core }) => { const [shouldPostBot, botMessage] = shouldSendBotReply(); if (shouldPostBot) { - // post bot reply only when there are no previous - // bot comments in past hour to prevent overwhelming - // issue comment section - let recentBotComments = await findRecentCommentsByUser(LE_BOT_USERNAME); - if (recentBotComments.length > 0) { - const slackBotSkippedMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${escapedTitle}> (less than 1 hour since last bot reply)*`; + // post bot reply only when there are no same bot comments + // in the past hour to prevent overwhelming issue comment section + const skipBot = await hasRecentBotComment( + issueNumber, + LE_BOT_USERNAME, + botMessage, + 3600000, + { github, context, core } + ); + if (skipBot) { + const slackBotSkippedMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${issueTitle}> (less than 1 hour since last bot message)*`; core.setOutput('bot_reply_skipped', true); core.setOutput('slack_notification_bot_skipped', slackBotSkippedMessage); } else { - const botReplyUrl = await sendBotReply(botMessage); - if (botReplyUrl) { - const slackMessage = `*[${repo}] <${botReplyUrl}|Bot response sent> on issue: <${issueUrl}|${escapedTitle}>*`; + const botMessageUrl = await sendBotMessage(issueNumber, botMessage, { github, context, core }); + if (botMessageUrl) { + const slackMessage = `*[${repo}] <${botMessageUrl}|Bot response sent> on issue: <${issueUrl}|${issueTitle}>*`; core.setOutput('bot_replied', true); core.setOutput('slack_notification_bot_comment', slackMessage); } @@ -200,7 +176,7 @@ module.exports = async ({ github, context, core }) => { } const contactSupport = shouldContactSupport(); - let slackMessage = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${escapedTitle}> by _${commentAuthor}_*`; + let slackMessage = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${issueTitle}> by _${commentAuthor}_*`; if (contactSupport) { // Append activity info when sending to #support-dev diff --git a/scripts/contributor-pr-reply.js b/scripts/contributor-pr-reply.js new file mode 100644 index 0000000..b35f128 --- /dev/null +++ b/scripts/contributor-pr-reply.js @@ -0,0 +1,23 @@ +const { BOT_MESSAGE_PULL_REQUEST } = require('./constants'); +const { sendBotMessage } = require('./utils'); + +module.exports = async ({ github, context, core }) => { + try { + const repo = context.repo.repo; + const number = context.payload.pull_request.number; + const url = context.payload.pull_request.html_url; + const title = context.payload.pull_request.title; + + const botMessageUrl = await sendBotMessage(number, BOT_MESSAGE_PULL_REQUEST, { github, context, core }); + + if (botMessageUrl) { + const slackMessage = `*[${repo}] <${botMessageUrl}|Reply sent> on pull request: <${url}|${title}>*`; + core.setOutput('slack_notification', slackMessage); + } else { + core.setOutput('slack_notification', ''); + } + } catch (error) { + core.setOutput('slack_notification', ''); + core.setFailed(`Action failed with error: ${error.message}`); + } +}; diff --git a/scripts/holiday-message.js b/scripts/holiday-message.js new file mode 100644 index 0000000..eff2fb8 --- /dev/null +++ b/scripts/holiday-message.js @@ -0,0 +1,60 @@ +// See docs/community-automations.md + +const { HOLIDAY_MESSAGE, LE_BOT_USERNAME } = require('./constants'); +const { + sendBotMessage, + escapeIssueTitleForSlackMessage, + hasRecentBotComment +} = require('./utils'); + +module.exports = async ({ github, context, core }) => { + try { + const repo = context.repo.repo; + + const isPullRequest = !!context.payload.pull_request; + const number = isPullRequest + ? context.payload.pull_request.number + : context.payload.issue.number; + const url = isPullRequest + ? context.payload.pull_request.html_url + : context.payload.issue.html_url; + const title = escapeIssueTitleForSlackMessage( + isPullRequest + ? context.payload.pull_request.title + : context.payload.issue.title + ); + + // post bot reply only when there are no same bot comments + // in the past hour to prevent overwhelming issue comment section + const skipBot = await hasRecentBotComment( + number, + LE_BOT_USERNAME, + HOLIDAY_MESSAGE, + 3600000, + { github, context, core } + ); + if (skipBot) { + const itemType = isPullRequest ? 'pull request' : 'issue'; + const slackBotSkippedMessage = `*[${repo}] Holiday message skipped on ${itemType}: <${url}|${title}> (less than 1 hour since last holiday message)*`; + core.setOutput('slack_notification', slackBotSkippedMessage); + return; + } + + const botMessageUrl = await sendBotMessage( + number, + HOLIDAY_MESSAGE, + { github, context, core } + ); + + if (botMessageUrl) { + const itemType = isPullRequest ? 'pull request' : 'issue'; + const slackMessage = `*[${repo}] <${botMessageUrl}|Holiday message sent> on ${itemType}: <${url}|${title}>*`; + core.setOutput('slack_notification', slackMessage); + } else { + core.setOutput('slack_notification', ''); + } + } catch (error) { + core.setOutput('slack_notification', ''); + core.setFailed(`Action failed with error: ${error.message}`); + } +}; diff --git a/scripts/utils.js b/scripts/utils.js index c397be3..35cc026 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,4 +1,4 @@ -const { LE_BOT_USERNAME, SENTRY_BOT_USERNAME } = require('./constants'); +const { LE_BOT_USERNAME, SENTRY_BOT_USERNAME, HOLIDAY_MESSAGE_START_DATE, HOLIDAY_MESSAGE_END_DATE } = require('./constants'); const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); /** @@ -96,8 +96,71 @@ async function isCloseContributor(username, { github, context, core }) { } } +function isHolidayMessageActive(currentDate = new Date()) { + return currentDate >= HOLIDAY_MESSAGE_START_DATE && currentDate <= HOLIDAY_MESSAGE_END_DATE; +} + +/** + * Sends a bot message as a comment on an issue. Returns message URL if successful. + */ +async function sendBotMessage(issueNumber, message, { github, context, core }) { + try { + if (!issueNumber) { + throw new Error('Issue number is required'); + } + if (!message) { + throw new Error('Message content is required'); + } + + const response = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: message, + }); + + if (!response?.data?.html_url) { + throw new Error('Comment created but no URL returned'); + } + + return response.data.html_url; + } catch (error) { + throw new Error(error.message); + } +} + +function escapeIssueTitleForSlackMessage(issueTitle) { + return issueTitle.replace(/"/g, '\\"').replace(//g, '>'); +} + +/** + * Checks if a bot sent a message with a given text on an issue + * in the past specified milliseconds. + */ +async function hasRecentBotComment(issueNumber, botUsername, commentText, msAgo, { github, context, core }) { + const oneHourAgo = new Date(Date.now() - msAgo); + const owner = context.repo.owner; + const repo = context.repo.repo; + + try { + const response = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + since: oneHourAgo.toISOString() + }); + return (response.data || []).some(comment => comment.user && comment.user.login === botUsername && comment.body.includes(commentText)); + } catch (error) { + core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); + } +} + module.exports = { isContributor, isCloseContributor, - isBot + isBot, + isHolidayMessageActive, + sendBotMessage, + escapeIssueTitleForSlackMessage, + hasRecentBotComment, }; From 9f11d18b574ba140b7d335e7347e7dbbd71d7711 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:21 +0100 Subject: [PATCH 15/35] Simplification and fixes of how Slack channel is determined --- .../workflows/contributor-issue-comment.yml | 29 ++++---------- scripts/contributor-issue-comment.js | 40 +++++-------------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index eb31c93..fabfd4a 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -70,38 +70,25 @@ jobs: script: | const script = require('./scripts/contributor-issue-comment.js'); return await script({ github, context, core }); - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - - name: Send Slack notification about GitHub comment + - name: Send Slack notification to support-dev channel + if : ${{ steps.script.outputs.support_dev_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook - webhook: ${{ steps.script.outputs.webhook_url }} + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} payload: > { - "text": "${{ steps.script.outputs.slack_notification_comment }}" + "text": "${{ steps.script.outputs.support_dev_message }}" } - - name: Send Slack notification about GitHub bot reply - if: ${{ steps.script.outputs.bot_replied }} + - name: Send Slack notification to support-dev-notifications channel + if : ${{ steps.script.outputs.support_dev_notifications_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook - webhook: ${{ steps.script.outputs.webhook_url }} + webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} payload: > { - "text": "${{ steps.script.outputs.slack_notification_bot_comment }}" - } - - - name: Send Slack notification about skipped GitHub bot reply - if: ${{ steps.script.outputs.bot_reply_skipped }} - uses: slackapi/slack-github-action@v2.1.0 - with: - webhook-type: incoming-webhook - webhook: ${{ steps.script.outputs.webhook_url }} - payload: > - { - "text": "${{ steps.script.outputs.slack_notification_bot_skipped }}" + "text": "${{ steps.script.outputs.support_dev_notifications_message }}" } diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 6149efc..59aa6f7 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -26,8 +26,6 @@ module.exports = async ({ github, context, core }) => { const commentBody = context.payload.comment.body; const repo = context.repo.repo; const owner = context.repo.owner; - const supportDevSlackWebhookUrl = process.env.SLACK_WEBHOOK_URL; - const supportDevNotificationsSlackWebhookUrl = process.env.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL; const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST .map(k => k.trim().toLowerCase()) .filter(Boolean) @@ -105,15 +103,11 @@ module.exports = async ({ github, context, core }) => { } function shouldSendBotReply() { + if (commentAuthorIsCloseContributor) { + return [false, null]; + } + if (issueCreator === commentAuthor) { - // Strictly prevents all bot replies on issues reported - // by people using our apps - sometimes there's conversation - // in comments and in this context, it's strange to have the bot - // chime in if someone uses a keyword triggering the bot. - // (This means that a bot reply won't be sent if a contributor - // reports an issue and then requests its assignment. But that's - // acceptable trade-off, and we'd likely want to see the report anyway - // and then decide whether they can work on it). return [false, null]; } @@ -129,19 +123,14 @@ module.exports = async ({ github, context, core }) => { } function shouldContactSupport() { - // for close contributors, send notification - // to #support-dev under all circumstances if (commentAuthorIsCloseContributor) { return true; } - // for other contributors, do not send on non-help-wanted issues if (!isHelpWanted) { return false; } - // on help-wanted issues, do not send if it's an assignment - // request and issue is already assigned to someone else if (isAssignmentRequest && isIssueAssignedToSomeoneElse) { return false; } @@ -161,16 +150,13 @@ module.exports = async ({ github, context, core }) => { { github, context, core } ); if (skipBot) { - const slackBotSkippedMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${issueTitle}> (less than 1 hour since last bot message)*`; - - core.setOutput('bot_reply_skipped', true); - core.setOutput('slack_notification_bot_skipped', slackBotSkippedMessage); + const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${issueTitle}> (less than 1 hour since last bot message)*`; + core.setOutput('support_dev_notifications_message', slackMessage); } else { const botMessageUrl = await sendBotMessage(issueNumber, botMessage, { github, context, core }); if (botMessageUrl) { const slackMessage = `*[${repo}] <${botMessageUrl}|Bot response sent> on issue: <${issueUrl}|${issueTitle}>*`; - core.setOutput('bot_replied', true); - core.setOutput('slack_notification_bot_comment', slackMessage); + core.setOutput('support_dev_notifications_message', slackMessage); } } } @@ -179,23 +165,15 @@ module.exports = async ({ github, context, core }) => { let slackMessage = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${issueTitle}> by _${commentAuthor}_*`; if (contactSupport) { - // Append activity info when sending to #support-dev - // to guide the decision on whether to assign an issue const [assignedOpenIssues, openPRs] = await Promise.all([ getIssues(commentAuthor, 'open'), getPullRequests(commentAuthor, 'open') ]); const authorActivity = formatAuthorActivity(assignedOpenIssues, openPRs); slackMessage += ` _${authorActivity}_`; - - core.setOutput('webhook_url', supportDevSlackWebhookUrl); - core.setOutput('slack_notification_comment', slackMessage); + core.setOutput('support_dev_message', slackMessage); } else { - // if we're not sending notification to #support-dev, - // send it to #support-dev-notifications and if the comment - // appears to be an assignment request, post GitHub bot reply - core.setOutput('webhook_url', supportDevNotificationsSlackWebhookUrl); - core.setOutput('slack_notification_comment', slackMessage); + core.setOutput('support_dev_notifications_message', slackMessage); } } catch (error) { core.setFailed(`Action failed with error: ${error.message}`); From 1abd76e4188892d5afcf88effabd61cd27816b23 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:24 +0100 Subject: [PATCH 16/35] Optimize notifications for assigned issue --- docs/community-automations.md | 5 +++-- scripts/contributor-issue-comment.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/community-automations.md b/docs/community-automations.md index ebe4b23..13041d5 100644 --- a/docs/community-automations.md +++ b/docs/community-automations.md @@ -10,8 +10,9 @@ Manages GitHub issue comments. Sends Slack notifications and GitHub bot replies. | **Issue creator** | Private | Any | No | Yes | No | - | | **Other** | Private | Regular | No | Yes | No | - | | **Other** | Private | Assignment request | No | Yes | Yes`*` | `BOT_MESSAGE_ISSUE_NOT_OPEN` | -| **Other** | `help-wanted` | Regular | **Yes** | No | No | - | -| **Other** | Unassigned `help-wanted` | Assignment request | **Yes** | No | No | - | +| **Other** | Unassigned `help-wanted` | Any | **Yes** | No | No | - | +| **Other** | `help-wanted` assigned to the comment author | Any | **Yes** | No | No | - | +| **Other** | `help-wanted` assigned to someone else | Regular | No | Yes | No | - | | **Other** | `help-wanted` assigned to someone else | Assignment request | No | Yes | Yes`*` | `BOT_MESSAGE_ALREADY_ASSIGNED` | `*` There is an additional optimization that prevents more than one bot message per hour to not overwhelm issue comment section diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 59aa6f7..e262498 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -111,7 +111,7 @@ module.exports = async ({ github, context, core }) => { return [false, null]; } - if (isHelpWanted && isAssignmentRequest && isIssueAssignedToSomeoneElse) { + if (isHelpWanted && isIssueAssignedToSomeoneElse && isAssignmentRequest) { return [true, BOT_MESSAGE_ALREADY_ASSIGNED]; } @@ -131,7 +131,7 @@ module.exports = async ({ github, context, core }) => { return false; } - if (isAssignmentRequest && isIssueAssignedToSomeoneElse) { + if (isHelpWanted && isIssueAssignedToSomeoneElse) { return false; } From 1f33613d9a679bb1babacd449fc4aefae7531a7a Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:27 +0100 Subject: [PATCH 17/35] Simplify and fix conditions --- .github/workflows/contributor-issue-comment.yml | 4 ++-- .github/workflows/holiday-message.yml | 2 +- scripts/holiday-message.js | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index fabfd4a..760f20c 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -72,7 +72,7 @@ jobs: return await script({ github, context, core }); - name: Send Slack notification to support-dev channel - if : ${{ steps.script.outputs.support_dev_message }} + if: ${{ steps.script.outputs.support_dev_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook @@ -83,7 +83,7 @@ jobs: } - name: Send Slack notification to support-dev-notifications channel - if : ${{ steps.script.outputs.support_dev_notifications_message }} + if: ${{ steps.script.outputs.support_dev_notifications_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 3ac6781..55e40ad 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -71,7 +71,7 @@ jobs: return await script({ github, context, core }); - name: Send Slack notification - if: ${{ steps.script.outputs.slack_notification != ''}} + if: ${{ steps.script.outputs.slack_notification }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook diff --git a/scripts/holiday-message.js b/scripts/holiday-message.js index eff2fb8..8577996 100644 --- a/scripts/holiday-message.js +++ b/scripts/holiday-message.js @@ -50,11 +50,8 @@ module.exports = async ({ github, context, core }) => { const itemType = isPullRequest ? 'pull request' : 'issue'; const slackMessage = `*[${repo}] <${botMessageUrl}|Holiday message sent> on ${itemType}: <${url}|${title}>*`; core.setOutput('slack_notification', slackMessage); - } else { - core.setOutput('slack_notification', ''); } } catch (error) { - core.setOutput('slack_notification', ''); core.setFailed(`Action failed with error: ${error.message}`); } }; From 0618db8d902c388e9f8ba37860043b901c118b2a Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:30 +0100 Subject: [PATCH 18/35] Fix missing Slack notification about bot --- .github/workflows/contributor-issue-comment.yml | 13 ++++++++++++- scripts/contributor-issue-comment.js | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index 760f20c..361dbff 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -82,7 +82,7 @@ jobs: "text": "${{ steps.script.outputs.support_dev_message }}" } - - name: Send Slack notification to support-dev-notifications channel + - name: Send Slack notification to support-dev-notifications channel about comment if: ${{ steps.script.outputs.support_dev_notifications_message }} uses: slackapi/slack-github-action@v2.1.0 with: @@ -92,3 +92,14 @@ jobs: { "text": "${{ steps.script.outputs.support_dev_notifications_message }}" } + + - name: Send Slack notification to support-dev-notifications channel about bot action + if: ${{ steps.script.outputs.support_dev_notifications_bot }} + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} + payload: > + { + "text": "${{ steps.script.outputs.support_dev_notifications_bot }}" + } diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index e262498..e018c0d 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -151,12 +151,12 @@ module.exports = async ({ github, context, core }) => { ); if (skipBot) { const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${issueTitle}> (less than 1 hour since last bot message)*`; - core.setOutput('support_dev_notifications_message', slackMessage); + core.setOutput('support_dev_notifications_bot', slackMessage); } else { const botMessageUrl = await sendBotMessage(issueNumber, botMessage, { github, context, core }); if (botMessageUrl) { const slackMessage = `*[${repo}] <${botMessageUrl}|Bot response sent> on issue: <${issueUrl}|${issueTitle}>*`; - core.setOutput('support_dev_notifications_message', slackMessage); + core.setOutput('support_dev_notifications_bot', slackMessage); } } } From ef571f120bfb82f751490136b4f5ae21ce295054 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 10:28:32 +0100 Subject: [PATCH 19/35] Finalize PR message --- scripts/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/constants.js b/scripts/constants.js index e9013ad..01cb4bf 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -40,7 +40,7 @@ const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your intere const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; -const BOT_MESSAGE_PULL_REQUEST = `Welcome! ๐Ÿ‘‹ \n\n We will assign a reviewerโ€”typically within next two weeks. In the meantime, to ensure a smooth review process, please **check that linting and tests pass, all issue requirements are satisfied, and that your contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai).** We'll be in touch! ๐Ÿ˜Š`; +const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure:\n\n- [ ] **Linting and tests pass**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; // Holiday message will be sent between the following datetimes // Additionally before/after holidays, enable/disable all related workflows From f3b6493395c3209c5d7e2abab64f86400da72b08 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 18:00:21 +0100 Subject: [PATCH 20/35] Install lint,prettier,precommit --- .eslintrc.js | 33 ++ .gitignore | 1 + .pre-commit-config.yaml | 25 ++ .prettierignore | 3 + .prettierrc.js | 11 + package.json | 9 + requirements-dev.txt | 1 + yarn.lock | 788 ++++++++++++++++++++++++++++++++++++---- 8 files changed, 807 insertions(+), 64 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .pre-commit-config.yaml create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 requirements-dev.txt diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..966e5e3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +var ERROR = 2; + +module.exports = { + env: { + es6: true, + browser: true, + node: true, + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + ecmaFeatures: { + impliedStrict: true, + }, + }, + extends: [ + 'eslint:recommended', + ], + rules: { + 'comma-style': ERROR, + 'no-console': ERROR, + 'max-len': [ + ERROR, + 100, + { + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreUrls: true, + ignoreTrailingComments: true, + }, + ], + }, +}; diff --git a/.gitignore b/.gitignore index c74ff1b..f3ba074 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env +.venv /node_modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..acf3146 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: check-added-large-files + exclude: '^tests/cassettes' + - id: debug-statements + - id: end-of-file-fixer + exclude: '^.+?\.json$' + - repo: https://github.com/google/yamlfmt + rev: v0.14.0 + hooks: + - id: yamlfmt + exclude: '^tests/cassettes' + - repo: https://github.com/rhysd/actionlint + rev: v1.7.7 + hooks: + - id: actionlint + additional_dependencies: + # actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions + # and checks these with shellcheck. This is arguably its most useful feature, + # but the integration only works if shellcheck is installed + - "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..51ddf53 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +*.md diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..7aee780 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,11 @@ +/* + * Configuration object for prettier options, should be used wherever prettier is invoked. + */ + +module.exports = { + printWidth: 100, + singleQuote: true, + arrowParens: 'avoid', + vueIndentScriptAndStyle: true, + singleAttributePerLine: true, +}; diff --git a/package.json b/package.json index 6fb5939..b049a7c 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,18 @@ { + "scripts": { + "lint": "eslint .", + "format": "prettier --write ." + }, "dependencies": { "axios": "^1.7.5", "dotenv": "^16.4.5", "googleapis": "^142.0.0", "node-fetch-native": "^1.6.4", "path": "^0.12.7" + }, + "devDependencies": { + "eslint": "^8.57.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.6.2" } } diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..416634f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pre-commit diff --git a/yarn.lock b/yarn.lock index 74d2e50..f20ead1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,43 +2,170 @@ # yarn lockfile v1 +"@eslint-community/eslint-utils@^4.2.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.6.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@ungap/structured-clone@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + agent-base@^7.1.2: version "7.1.4" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== axios@^1.7.5: version "1.10.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" + resolved "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz" integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base64-js@^1.3.0: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== bignumber.js@^9.0.0: version "9.3.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz" integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + buffer-equal-constant-time@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -46,39 +173,90 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: call-bound@^1.0.2: version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -debug@4: +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.2: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@4, debug@^4.3.1, debug@^4.3.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dotenv@^16.4.5: version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -87,31 +265,31 @@ dunder-proto@^1.0.1: ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -119,19 +297,170 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^10.1.8: + version "10.1.8" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz" + integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.57.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + extend@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + follow-redirects@^1.15.6: version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== form-data@^4.0.0: version "4.0.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.3.tgz#608b1b3f3e28be0fccf5901fc85fb3641e5cf0ae" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz" integrity sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA== dependencies: asynckit "^0.4.0" @@ -140,14 +469,19 @@ form-data@^4.0.0: hasown "^2.0.2" mime-types "^2.1.12" +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== gaxios@^6.0.0, gaxios@^6.0.3, gaxios@^6.1.1: version "6.7.1" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + resolved "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz" integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== dependencies: extend "^3.0.2" @@ -158,7 +492,7 @@ gaxios@^6.0.0, gaxios@^6.0.3, gaxios@^6.1.1: gcp-metadata@^6.1.0: version "6.1.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz" integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== dependencies: gaxios "^6.1.1" @@ -167,7 +501,7 @@ gcp-metadata@^6.1.0: get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -183,15 +517,41 @@ get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + google-auth-library@^9.0.0, google-auth-library@^9.7.0: version "9.15.1" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz" integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== dependencies: base64-js "^1.3.0" @@ -203,12 +563,12 @@ google-auth-library@^9.0.0, google-auth-library@^9.7.0: google-logging-utils@^0.0.2: version "0.0.2" - resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + resolved "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz" integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== googleapis-common@^7.0.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-7.2.0.tgz#5c19102c9af1e5d27560be5e69ee2ccf68755d42" + resolved "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz" integrity sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA== dependencies: extend "^3.0.2" @@ -220,7 +580,7 @@ googleapis-common@^7.0.0: googleapis@^142.0.0: version "142.0.0" - resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-142.0.0.tgz#fa7a0414ab6ec935554fe52eeba067602676a9c7" + resolved "https://registry.npmjs.org/googleapis/-/googleapis-142.0.0.tgz" integrity sha512-LsU1ynez4/KNPwnFMSDI93pBEsETNdQPCrT3kz2qgiNg5H2pW4dKW+1VmENMkZ4u9lMxA89nnXD3nqWBJ0rruQ== dependencies: google-auth-library "^9.0.0" @@ -228,64 +588,149 @@ googleapis@^142.0.0: gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + gtoken@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz" integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== dependencies: gaxios "^6.0.0" jws "^4.0.0" +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" https-proxy-agent@^7.0.1: version "7.0.6" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz" integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== dependencies: agent-base "^7.1.2" debug "4" +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + inherits@2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + json-bigint@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz" integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== dependencies: bignumber.js "^9.0.0" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + jwa@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz" integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== dependencies: buffer-equal-constant-time "^1.0.1" @@ -294,84 +739,234 @@ jwa@^2.0.0: jws@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz" integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== dependencies: jwa "^2.0.0" safe-buffer "^5.0.1" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mime-db@1.52.0: version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + node-fetch-native@^1.6.4: version "1.6.6" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37" + resolved "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz" integrity sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ== node-fetch@^2.6.9: version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" object-inspect@^1.13.3: version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path@^0.12.7: version "0.12.7" - resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + resolved "https://registry.npmjs.org/path/-/path-0.12.7.tgz" integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== dependencies: process "^0.11.1" util "^0.10.3" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + process@^0.11.1: version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qs@^6.7.0: version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + resolved "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz" integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: side-channel "^1.1.0" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@^5.0.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + side-channel-list@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: es-errors "^1.3.0" @@ -379,7 +974,7 @@ side-channel-list@^1.0.0: side-channel-map@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== dependencies: call-bound "^1.0.2" @@ -389,7 +984,7 @@ side-channel-map@^1.0.1: side-channel-weakmap@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== dependencies: call-bound "^1.0.2" @@ -400,7 +995,7 @@ side-channel-weakmap@^1.0.2: side-channel@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: es-errors "^1.3.0" @@ -409,37 +1004,102 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + tr46@~0.0.3: version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + url-template@^2.0.8: version "2.0.8" - resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" + resolved "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz" integrity sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw== util@^0.10.3: version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + resolved "https://registry.npmjs.org/util/-/util-0.10.4.tgz" integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== dependencies: inherits "2.0.3" uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== webidl-conversions@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== whatwg-url@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 899ae780cab6011926550046a899622694649c28 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Mon, 10 Nov 2025 18:35:26 +0100 Subject: [PATCH 21/35] Disable detection of close contributors based on gh teams --- scripts/utils.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/utils.js b/scripts/utils.js index 35cc026..58bb619 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -57,9 +57,18 @@ async function isCloseContributor(username, { github, context, core }) { if (CLOSE_CONTRIBUTORS.map(c => c.toLowerCase().trim()).includes(username.toLowerCase().trim())) { return true; + } else { + return false; } - const org = context.repo.owner; + // Detection on GitHub teams below is disabled until we re-think + // how close contributors are managed (see Notion tracker): + // - it was only fallback as explained lower + // - it causes the problem when we receive undesired notification + // to #support-dev when Richard posts issue comment since he is a member + // of GSoC and other GitHub teams with close contributors (they require moderator). + + /* const org = context.repo.owner; // Even though we check on team members here, it's best // to add everyone to CLOSE_CONTRIBUTORS constant anyway @@ -93,7 +102,7 @@ async function isCloseContributor(username, { github, context, core }) { } catch (error) { core.setFailed(error.message); return false; - } + } */ } function isHolidayMessageActive(currentDate = new Date()) { @@ -111,18 +120,18 @@ async function sendBotMessage(issueNumber, message, { github, context, core }) { if (!message) { throw new Error('Message content is required'); } - + const response = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: message, }); - + if (!response?.data?.html_url) { throw new Error('Comment created but no URL returned'); } - + return response.data.html_url; } catch (error) { throw new Error(error.message); From 8df6973e3b45965dd8708e9ed968c79d6dc7dde4 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Wed, 3 Dec 2025 10:11:56 +0100 Subject: [PATCH 22/35] Remove shellcheck installation As per https://github.com/learningequality/.github/pull/35#discussion_r2582149733 --- .pre-commit-config.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acf3146..09b0ab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,8 +18,5 @@ repos: rev: v1.7.7 hooks: - id: actionlint - additional_dependencies: - # actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions - # and checks these with shellcheck. This is arguably its most useful feature, - # but the integration only works if shellcheck is installed - - "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0" + # Expects shellcheck to be installed on the system + # https://github.com/koalaman/shellcheck#installing From 02f6b6f7e6c8df56afce745928eb938689fc5ff1 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Wed, 3 Dec 2025 10:46:01 +0100 Subject: [PATCH 23/35] Run linters --- .eslintrc.js | 4 +- .github/ISSUE_TEMPLATE/epic_issue.md | 14 +- .../call-contributor-issue-comment.yml | 2 - .../workflows/call-contributor-pr-reply.yml | 2 - .github/workflows/call-holiday-message.yml | 4 +- .../workflows/call-manage-issue-header.yml | 6 +- .../workflows/call-update-pr-spreadsheet.yml | 14 +- .../community-contribution-label.yml | 40 ++- .../workflows/contributor-issue-comment.yml | 33 +-- .github/workflows/contributor-pr-reply.yml | 16 +- .github/workflows/holiday-message.yml | 17 +- .github/workflows/is-contributor.yml | 10 +- .github/workflows/is-holiday-active.yml | 10 +- .github/workflows/manage-issue-header.yml | 14 +- .../workflows/unassign-inactive-issues.yaml | 30 +- .github/workflows/update-pr-spreadsheet.yml | 11 +- scripts/constants.js | 69 ++++- scripts/contributor-issue-comment.js | 275 ++++++++++-------- scripts/contributor-pr-reply.js | 6 +- scripts/get_community_assigned_issues.py | 20 +- scripts/holiday-message.js | 34 +-- scripts/is-contributor.js | 6 +- scripts/manage-issue-header.js | 35 ++- scripts/unassign-inactive-issues.js | 142 +++++---- scripts/update-pr-spreadsheet.js | 52 ++-- scripts/utils.js | 29 +- 26 files changed, 455 insertions(+), 440 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 966e5e3..7231a22 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,9 +13,7 @@ module.exports = { impliedStrict: true, }, }, - extends: [ - 'eslint:recommended', - ], + extends: ['eslint:recommended'], rules: { 'comma-style': ERROR, 'no-console': ERROR, diff --git a/.github/ISSUE_TEMPLATE/epic_issue.md b/.github/ISSUE_TEMPLATE/epic_issue.md index 922c8c2..3f3386b 100644 --- a/.github/ISSUE_TEMPLATE/epic_issue.md +++ b/.github/ISSUE_TEMPLATE/epic_issue.md @@ -9,7 +9,7 @@ assignees: '' _[General guidance: When drafting a feature project, the goal is to create a reference for project scope and context. Write for our core product team. The issue should not be overly technical, and should be be comprehensible and a useful reference to devs, designers, and QA team, to build a shared source of understanding. Anyone at LE should be able to read this and more or less understand the project. If there is relevant information or context that is for the internal team only, please add that in a notion page and link it, rather than adding directly to these issues.]_ -## Overview +## Overview _[First, the project and purpose in one sentence, i.e.: Allow coaches and admins to customize quiz questions in Koilbri]_ @@ -18,11 +18,11 @@ _[Then, more detail]_ - Why are we doing this now? - What feedback, requests, insights and/or concerns are shaping this feature? Include specific comments from partners, clearinghouse, or teammates as relevant. -### Contributors -_[Fill this out as people are assigned]_ +### Contributors +_[Fill this out as people are assigned]_ - Technical lead: [github handle] -- Individual contributors: [list all github handles] -- Designer: [if a specific designer is leading] +- Individual contributors: [list all github handles] +- Designer: [if a specific designer is leading] - Other relevant LE team members: [optional, i.e. imps point person] ### Goals @@ -30,11 +30,11 @@ _[Fill this out as people are assigned]_ - How do we know this feature is successful? #### User Stories or Requirements -_[Break down the main workflows thematically, but still at a high level. How are the goals of the project realized in the UI? If there isn't a UI portion of this project, what are the key outcomes or improvements? There should probably only be a few of these per project]_ +_[Break down the main workflows thematically, but still at a high level. How are the goals of the project realized in the UI? If there isn't a UI portion of this project, what are the key outcomes or improvements? There should probably only be a few of these per project]_ ### Target Quarter and Due date - What is the timeline of this project? - If there is a specific due date, especially for a contractual deliverable, include it. Otherwise, include an estimated delivery date, and what quarter(s) this will be worked on -### Product Issues +### Product Issues _[Issues should be linked here as they are created. There will probably be 1-2 per User Story]_ diff --git a/.github/workflows/call-contributor-issue-comment.yml b/.github/workflows/call-contributor-issue-comment.yml index 4e8b7db..a9512b4 100644 --- a/.github/workflows/call-contributor-issue-comment.yml +++ b/.github/workflows/call-contributor-issue-comment.yml @@ -1,9 +1,7 @@ name: Handle contributor comment on GitHub issue - on: issue_comment: types: [created] - jobs: call-workflow: uses: learningequality/.github/.github/workflows/contributor-issue-comment.yml@main diff --git a/.github/workflows/call-contributor-pr-reply.yml b/.github/workflows/call-contributor-pr-reply.yml index b3ab284..e8316e0 100644 --- a/.github/workflows/call-contributor-pr-reply.yml +++ b/.github/workflows/call-contributor-pr-reply.yml @@ -1,9 +1,7 @@ name: Send reply on a new contributor pull request - on: pull_request_target: types: [opened] - jobs: call-workflow: name: Call shared workflow diff --git a/.github/workflows/call-holiday-message.yml b/.github/workflows/call-holiday-message.yml index 04d2ca8..6ed898b 100644 --- a/.github/workflows/call-holiday-message.yml +++ b/.github/workflows/call-holiday-message.yml @@ -1,11 +1,9 @@ name: Post holiday message on pull request or issue comment - on: pull_request_target: types: [opened] issue_comment: types: [created] - jobs: call-workflow: name: Call shared workflow @@ -13,4 +11,4 @@ jobs: secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} - SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} \ No newline at end of file + SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} diff --git a/.github/workflows/call-manage-issue-header.yml b/.github/workflows/call-manage-issue-header.yml index 4af6730..845a679 100644 --- a/.github/workflows/call-manage-issue-header.yml +++ b/.github/workflows/call-manage-issue-header.yml @@ -1,13 +1,11 @@ name: Manage issue header - on: issues: types: [opened, reopened, labeled, unlabeled] - jobs: call-workflow: name: Call shared workflow uses: learningequality/.github/.github/workflows/manage-issue-header.yml@main secrets: - LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} - LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/call-update-pr-spreadsheet.yml b/.github/workflows/call-update-pr-spreadsheet.yml index bb275a0..837a34e 100644 --- a/.github/workflows/call-update-pr-spreadsheet.yml +++ b/.github/workflows/call-update-pr-spreadsheet.yml @@ -1,16 +1,14 @@ name: Update community pull requests spreadsheet - on: pull_request_target: - types: [assigned,unassigned,opened,closed,reopened,edited,review_requested,review_request_removed] - + types: [assigned, unassigned, opened, closed, reopened, edited, review_requested, review_request_removed] jobs: call-workflow: name: Call shared workflow uses: learningequality/.github/.github/workflows/update-pr-spreadsheet.yml@main secrets: - LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} - LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} - CONTRIBUTIONS_SPREADSHEET_ID: ${{ secrets.CONTRIBUTIONS_SPREADSHEET_ID }} - CONTRIBUTIONS_SHEET_NAME: ${{ secrets.CONTRIBUTIONS_SHEET_NAME }} - GH_UPLOADER_GCP_SA_CREDENTIALS: ${{ secrets.GH_UPLOADER_GCP_SA_CREDENTIALS }} + LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} + LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} + CONTRIBUTIONS_SPREADSHEET_ID: ${{ secrets.CONTRIBUTIONS_SPREADSHEET_ID }} + CONTRIBUTIONS_SHEET_NAME: ${{ secrets.CONTRIBUTIONS_SHEET_NAME }} + GH_UPLOADER_GCP_SA_CREDENTIALS: ${{ secrets.GH_UPLOADER_GCP_SA_CREDENTIALS }} diff --git a/.github/workflows/community-contribution-label.yml b/.github/workflows/community-contribution-label.yml index 2e75764..f410caa 100644 --- a/.github/workflows/community-contribution-label.yml +++ b/.github/workflows/community-contribution-label.yml @@ -1,49 +1,43 @@ name: Community Contribution Label - on: - workflow_call: - secrets: - LE_BOT_APP_ID: - description: "GitHub App ID for authentication" - required: true - LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" - required: true + workflow_call: + secrets: + LE_BOT_APP_ID: + description: 'GitHub App ID for authentication' + required: true + LE_BOT_PRIVATE_KEY: + description: 'GitHub App Private Key for authentication' + required: true jobs: reusable-job: runs-on: ubuntu-latest - steps: - name: Check event context run: | - if [[ "${{ github.event_name }}" != "issues" ]] || [[ "${{ github.event.action }}" != "assigned" && "${{ github.event.action }}" != "unassigned" ]]; then - echo "This workflow should only run on issue assigned/unassigned events" - exit 1 - fi + if [[ "${{ github.event_name }}" != "issues" ]] || [[ "${{ github.event.action }}" != "assigned" && "${{ github.event.action }}" != "unassigned" ]]; then + echo "This workflow should only run on issue assigned/unassigned events" + exit 1 + fi - name: Checkout repository uses: actions/checkout@v4 with: - repository: learningequality/.github - path: .github-repo - + repository: learningequality/.github + path: .github-repo - name: Generate App Token id: generate-token uses: tibdex/github-app-token@v2 with: - app_id: ${{ secrets.LE_BOT_APP_ID }} - private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - + app_id: ${{ secrets.LE_BOT_APP_ID }} + private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Install dependencies run: | python -m pip install --upgrade pip pip install requests - - name: Add/Remove Community Contribution Label run: python .github-repo/scripts/community-contribution-label.py env: - token: ${{ steps.generate-token.outputs.token }} \ No newline at end of file + token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index 361dbff..4e182c5 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -1,22 +1,19 @@ name: Handle contributor comment on GitHub issue - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true SLACK_WEBHOOK_URL: required: true - description: "Webhook URL for Slack #support-dev channel" + description: 'Webhook URL for Slack #support-dev channel' SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: required: true - description: "Webhook URL for Slack #support-dev-notifications channel" - - + description: 'Webhook URL for Slack #support-dev-notifications channel' jobs: check-if-contributor: name: Check if author is contributor @@ -27,17 +24,10 @@ jobs: with: username: ${{ github.event.comment.user.login }} author_association: ${{ github.event.comment.author_association }} - process-issue-comment: name: Process issue comment needs: [check-if-contributor] - if: >- - ${{ - !github.event.issue.pull_request && - github.event.issue.state == 'open' && - needs.check-if-contributor.outputs.is_contributor == 'true' - }} - + if: ${{ !github.event.issue.pull_request && github.event.issue.state == 'open' && needs.check-if-contributor.outputs.is_contributor == 'true' }} runs-on: ubuntu-latest steps: - name: Generate App Token @@ -46,22 +36,18 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: script uses: actions/github-script@v7 @@ -70,36 +56,33 @@ jobs: script: | const script = require('./scripts/contributor-issue-comment.js'); return await script({ github, context, core }); - - name: Send Slack notification to support-dev channel if: ${{ steps.script.outputs.support_dev_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - payload: > + payload: | { "text": "${{ steps.script.outputs.support_dev_message }}" } - - name: Send Slack notification to support-dev-notifications channel about comment if: ${{ steps.script.outputs.support_dev_notifications_message }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - payload: > + payload: | { "text": "${{ steps.script.outputs.support_dev_notifications_message }}" } - - name: Send Slack notification to support-dev-notifications channel about bot action if: ${{ steps.script.outputs.support_dev_notifications_bot }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - payload: > + payload: | { "text": "${{ steps.script.outputs.support_dev_notifications_bot }}" } diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml index 2f5dd17..48df6a1 100644 --- a/.github/workflows/contributor-pr-reply.yml +++ b/.github/workflows/contributor-pr-reply.yml @@ -1,18 +1,16 @@ name: Send reply on a new contributor pull request - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: required: true - description: "Webhook URL for Slack #support-dev-notifications channel" - + description: 'Webhook URL for Slack #support-dev-notifications channel' jobs: check-if-contributor: name: Check if author is contributor @@ -23,7 +21,6 @@ jobs: with: username: ${{ github.event.pull_request.user.login }} author_association: ${{ github.event.pull_request.author_association }} - send-reply: name: Send reply needs: [check-if-contributor] @@ -36,22 +33,18 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: script uses: actions/github-script@v7 @@ -60,14 +53,13 @@ jobs: script: | const script = require('./scripts/contributor-pr-reply.js'); return await script({ github, context, core }); - - name: Send Slack notification if: ${{ steps.script.outputs.slack_notification != ''}} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - payload: > + payload: | { "text": "${{ steps.script.outputs.slack_notification }}" } diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 55e40ad..7a8314b 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -1,18 +1,16 @@ name: Post holiday message on pull request or issue comment - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: required: true - description: "Webhook URL for Slack #support-dev-notifications channel" - + description: 'Webhook URL for Slack #support-dev-notifications channel' jobs: check-holiday-active: name: Check if holiday message is active @@ -20,7 +18,6 @@ jobs: secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} - check-if-contributor: name: Check if author is contributor needs: [check-holiday-active] @@ -32,7 +29,6 @@ jobs: with: username: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login || github.event_name == 'issue_comment' && github.event.comment.user.login }} author_association: ${{ github.event_name == 'pull_request' && github.event.pull_request.author_association || github.event_name == 'issue_comment' && github.event.comment.author_association }} - post-holiday-message: name: Post holiday message needs: [check-holiday-active, check-if-contributor] @@ -45,22 +41,18 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: script uses: actions/github-script@v7 @@ -69,14 +61,13 @@ jobs: script: | const script = require('./scripts/holiday-message.js'); return await script({ github, context, core }); - - name: Send Slack notification if: ${{ steps.script.outputs.slack_notification }} uses: slackapi/slack-github-action@v2.1.0 with: webhook-type: incoming-webhook webhook: ${{ secrets.SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL }} - payload: > + payload: | { "text": "${{ steps.script.outputs.slack_notification }}" } diff --git a/.github/workflows/is-contributor.yml b/.github/workflows/is-contributor.yml index 4885077..4621daa 100644 --- a/.github/workflows/is-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -1,5 +1,4 @@ name: Check if user is contributor - on: workflow_call: inputs: @@ -18,9 +17,8 @@ on: required: true outputs: is_contributor: - description: "True if the user is a contributor (= not a core team member)" + description: 'True if the user is a contributor (= not a core team member)' value: ${{ jobs.check-contributor.outputs.is_contributor }} - jobs: check-contributor: name: Check if user is contributor @@ -34,21 +32,17 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: run-script uses: actions/github-script@v7 @@ -59,4 +53,4 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} script: | const script = require('./scripts/is-contributor.js'); - return await script({ core, github, context }); \ No newline at end of file + return await script({ core, github, context }); diff --git a/.github/workflows/is-holiday-active.yml b/.github/workflows/is-holiday-active.yml index 3534d79..0a53c4d 100644 --- a/.github/workflows/is-holiday-active.yml +++ b/.github/workflows/is-holiday-active.yml @@ -1,19 +1,17 @@ name: Check if holiday message is active - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true outputs: is_holiday_active: description: "True if we're within the holiday message period" value: ${{ jobs.check-holiday-active.outputs.is_holiday_active }} - jobs: check-holiday-active: name: Check if holiday message is active @@ -27,22 +25,18 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: script uses: actions/github-script@v7 diff --git a/.github/workflows/manage-issue-header.yml b/.github/workflows/manage-issue-header.yml index 531cfb2..7411b2a 100644 --- a/.github/workflows/manage-issue-header.yml +++ b/.github/workflows/manage-issue-header.yml @@ -1,15 +1,13 @@ name: Manage issue header - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" - required: true + description: 'GitHub App ID for authentication' + required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" - required: true - + description: 'GitHub App Private Key for authentication' + required: true jobs: manage-issue-header: runs-on: ubuntu-latest @@ -25,23 +23,19 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'yarn' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script uses: actions/github-script@v7 with: diff --git a/.github/workflows/unassign-inactive-issues.yaml b/.github/workflows/unassign-inactive-issues.yaml index d6b1942..0093279 100644 --- a/.github/workflows/unassign-inactive-issues.yaml +++ b/.github/workflows/unassign-inactive-issues.yaml @@ -1,19 +1,17 @@ name: Unassign Inactive Issues run-name: Unassigns users from issues if they are inactive based on comments or PR activity. - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true SLACK_COMMUNITY_NOTIFICATIONS_WEBHOOK_URL: - description: "Slack webhook URL for notifications" + description: 'Slack webhook URL for notifications' required: true - jobs: unassign_inactive_issue: runs-on: ubuntu-latest @@ -24,19 +22,16 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - - name: Checkout called repository - uses: actions/checkout@v3 + - name: Checkout .github repository + uses: actions/checkout@v4 with: repository: learningequality/.github ref: main - token: ${{ steps.generate-token.outputs.token }} - + token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '16' - + node-version: '20' - name: Debug directory run: | echo "Current directory:" @@ -45,19 +40,16 @@ jobs: ls -la echo "Scripts directory contents:" ls -la scripts - - name: Install dependencies - run: npm install - + run: npm install - name: Run unassign script id: unassign - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ steps.generate-token.outputs.token }} script: | const script = require('./scripts/unassign-inactive-issues.js'); return await script({github, context, core}); - - name: Send Slack notifications if: ${{ steps.unassign.outputs.unassignments != '' }} uses: slackapi/slack-github-action@v2.0.0 @@ -67,4 +59,4 @@ jobs: payload: | { "text": "\nThe following users have been unassigned due to inactivity:\n${{ steps.unassign.outputs.unassignments }}" - } \ No newline at end of file + } diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index e280342..f03ecc5 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -1,13 +1,12 @@ name: Update community pull requests spreadsheet - on: workflow_call: secrets: LE_BOT_APP_ID: - description: "GitHub App ID for authentication" + description: 'GitHub App ID for authentication' required: true LE_BOT_PRIVATE_KEY: - description: "GitHub App Private Key for authentication" + description: 'GitHub App Private Key for authentication' required: true CONTRIBUTIONS_SPREADSHEET_ID: required: true @@ -15,7 +14,6 @@ on: required: true GH_UPLOADER_GCP_SA_CREDENTIALS: required: true - jobs: check-if-contributor: name: Check if author is contributor @@ -26,7 +24,6 @@ jobs: with: username: ${{ github.event.pull_request.user.login }} author_association: ${{ github.event.pull_request.author_association }} - update-spreadsheet: needs: [check-if-contributor] runs-on: ubuntu-latest @@ -38,22 +35,18 @@ jobs: with: app_id: ${{ secrets.LE_BOT_APP_ID }} private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository uses: actions/checkout@v4 with: repository: learningequality/.github ref: main token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Run script id: script uses: actions/github-script@v7 diff --git a/scripts/constants.js b/scripts/constants.js index 01cb4bf..f044af1 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -6,24 +6,61 @@ const SENTRY_BOT_USERNAME = 'sentry-io[bot]'; // close contributors are treated a bit special in some workflows, // for example, we receive a high priority notification about their // comments on all issues rather than just on 'help wanted' issues -const CLOSE_CONTRIBUTORS = ['AadarshM07', 'Abhishek-Punhani', 'BabyElias', 'Dimi20cen', 'EshaanAgg', 'GarvitSinghal47', 'habibayman', 'iamshobhraj', 'indirectlylit', 'Jakoma02', 'KshitijThareja', 'muditchoudhary', 'nathanaelg16', 'nikkuAg', 'Sahil-Sinha-11', 'shivam-daksh', 'shruti862', 'thesujai', 'WinnyChang', 'yeshwanth235']; +const CLOSE_CONTRIBUTORS = [ + 'AadarshM07', + 'Abhishek-Punhani', + 'BabyElias', + 'Dimi20cen', + 'EshaanAgg', + 'GarvitSinghal47', + 'habibayman', + 'iamshobhraj', + 'indirectlylit', + 'Jakoma02', + 'KshitijThareja', + 'muditchoudhary', + 'nathanaelg16', + 'nikkuAg', + 'Sahil-Sinha-11', + 'shivam-daksh', + 'shruti862', + 'thesujai', + 'WinnyChang', + 'yeshwanth235', +]; const TEAMS_WITH_CLOSE_CONTRIBUTORS = ['gsoc-contributors', 'learning-equality-community-guide']; const KEYWORDS_DETECT_ASSIGNMENT_REQUEST = [ - 'assign', 'assigned', - 'work', 'working', - 'contribute', 'contributing', - 'request', 'requested', - 'pick', 'picked', 'picking', - 'address', 'addressing', - 'handle', 'handling', - 'solve', 'solving', 'resolve', 'resolving', - 'try', 'trying', - 'grab', 'grabbing', - 'claim', 'claimed', - 'interest', 'interested', - 'do', 'doing', + 'assign', + 'assigned', + 'work', + 'working', + 'contribute', + 'contributing', + 'request', + 'requested', + 'pick', + 'picked', + 'picking', + 'address', + 'addressing', + 'handle', + 'handling', + 'solve', + 'solving', + 'resolve', + 'resolving', + 'try', + 'trying', + 'grab', + 'grabbing', + 'claim', + 'claimed', + 'interest', + 'interested', + 'do', + 'doing', 'help', 'take', 'want', @@ -31,7 +68,7 @@ const KEYWORDS_DETECT_ASSIGNMENT_REQUEST = [ 'own', 'on it', 'available', - 'got this' + 'got this', ]; const ISSUE_LABEL_HELP_WANTED = 'help wanted'; @@ -62,5 +99,5 @@ module.exports = { TEAMS_WITH_CLOSE_CONTRIBUTORS, HOLIDAY_MESSAGE_START_DATE, HOLIDAY_MESSAGE_END_DATE, - HOLIDAY_MESSAGE + HOLIDAY_MESSAGE, }; diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index e018c0d..47c3ed8 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -5,15 +5,129 @@ const { KEYWORDS_DETECT_ASSIGNMENT_REQUEST, ISSUE_LABEL_HELP_WANTED, BOT_MESSAGE_ISSUE_NOT_OPEN, - BOT_MESSAGE_ALREADY_ASSIGNED + BOT_MESSAGE_ALREADY_ASSIGNED, } = require('./constants'); const { isCloseContributor, sendBotMessage, escapeIssueTitleForSlackMessage, - hasRecentBotComment + hasRecentBotComment, } = require('./utils'); +async function hasLabel(name, owner, repo, issueNumber, github, core) { + let labels = []; + try { + const response = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: issueNumber, + }); + labels = response.data.map(label => label.name); + } catch (error) { + core.warning(`Failed to fetch labels on issue #${issueNumber}: ${error.message}`); + labels = []; + } + return labels.some(label => label.toLowerCase() === name.toLowerCase()); +} + +async function getIssues(assignee, state, owner, repo, github, core) { + try { + const response = await github.rest.issues.listForRepo({ + owner, + repo, + assignee, + state, + }); + return response.data.filter(issue => !issue.pull_request); + } catch (error) { + core.warning(`Failed to fetch issues: ${error.message}`); + return []; + } +} + +async function getPullRequests(assignee, state, owner, repo, github, core) { + try { + const response = await github.rest.pulls.list({ + owner, + repo, + state, + }); + return response.data.filter(pr => pr.user.login === assignee); + } catch (error) { + core.warning(`Failed to fetch pull requests: ${error.message}`); + return []; + } +} + +// Format information about author's assigned open issues +// as '(Issues #1 #2 | PRs #3)' and PRs for Slack message +function formatAuthorActivity(issues, pullRequests) { + const parts = []; + + if (issues.length > 0) { + const issueLinks = issues.map(issue => `<${issue.html_url}|#${issue.number}>`).join(' '); + parts.push(`Issues ${issueLinks}`); + } else { + parts.push(`Issues none`); + } + + if (pullRequests.length > 0) { + const prLinks = pullRequests.map(pr => `<${pr.html_url}|#${pr.number}>`).join(' '); + parts.push(`PRs ${prLinks}`); + } else { + parts.push(`PRs none`); + } + + return `(${parts.join(' | ')})`; +} + +function shouldSendBotReply( + issueCreator, + commentAuthor, + commentAuthorIsCloseContributor, + isHelpWanted, + isAssignmentRequest, + isIssueAssignedToSomeoneElse, +) { + if (commentAuthorIsCloseContributor) { + return [false, null]; + } + + if (issueCreator === commentAuthor) { + return [false, null]; + } + + if (isHelpWanted && isIssueAssignedToSomeoneElse && isAssignmentRequest) { + return [true, BOT_MESSAGE_ALREADY_ASSIGNED]; + } + + if (!isHelpWanted && isAssignmentRequest) { + return [true, BOT_MESSAGE_ISSUE_NOT_OPEN]; + } + + return [false, null]; +} + +function shouldContactSupport( + commentAuthorIsCloseContributor, + isHelpWanted, + isIssueAssignedToSomeoneElse, +) { + if (commentAuthorIsCloseContributor) { + return true; + } + + if (!isHelpWanted) { + return false; + } + + if (isHelpWanted && isIssueAssignedToSomeoneElse) { + return false; + } + + return true; +} + module.exports = async ({ github, context, core }) => { try { const issueNumber = context.payload.issue.number; @@ -26,134 +140,51 @@ module.exports = async ({ github, context, core }) => { const commentBody = context.payload.comment.body; const repo = context.repo.repo; const owner = context.repo.owner; - const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST - .map(k => k.trim().toLowerCase()) + const keywordRegexes = KEYWORDS_DETECT_ASSIGNMENT_REQUEST.map(k => k.trim().toLowerCase()) .filter(Boolean) .map(keyword => new RegExp(`\\b${keyword}\\b`, 'i')); const isAssignmentRequest = keywordRegexes.find(regex => regex.test(commentBody)); - const isIssueAssignedToSomeoneElse = issueAssignees && issueAssignees.length > 0 && !issueAssignees.includes(commentAuthor); - const isHelpWanted = await hasLabel(ISSUE_LABEL_HELP_WANTED); - const commentAuthorIsCloseContributor = await isCloseContributor(commentAuthor, { github, context, core }); - - async function hasLabel(name) { - let labels = []; - try { - const response = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: issueNumber - }); - labels = response.data.map(label => label.name); - } catch (error) { - core.warning(`Failed to fetch labels on issue #${issueNumber}: ${error.message}`); - labels = []; - } - return labels.some(label => label.toLowerCase() === name.toLowerCase()); - } - - async function getIssues(assignee, state) { - try { - const response = await github.rest.issues.listForRepo({ - owner, - repo, - assignee, - state - }); - return response.data.filter(issue => !issue.pull_request); - } catch (error) { - core.warning(`Failed to fetch issues: ${error.message}`); - return []; - } - } - - async function getPullRequests(assignee, state) { - try { - const response = await github.rest.pulls.list({ - owner, - repo, - state - }); - return response.data.filter(pr => pr.user.login === assignee); - } catch (error) { - core.warning(`Failed to fetch pull requests: ${error.message}`); - return []; - } - } - - // Format information about author's assigned open issues - // as '(Issues #1 #2 | PRs #3)' and PRs for Slack message - function formatAuthorActivity(issues, pullRequests) { - const parts = []; - - if (issues.length > 0) { - const issueLinks = issues.map(issue => `<${issue.html_url}|#${issue.number}>`).join(' '); - parts.push(`Issues ${issueLinks}`); - } else { - parts.push(`Issues none`); - } - - if (pullRequests.length > 0) { - const prLinks = pullRequests.map(pr => `<${pr.html_url}|#${pr.number}>`).join(' '); - parts.push(`PRs ${prLinks}`); - } else { - parts.push(`PRs none`); - } - - return `(${parts.join(' | ')})`; - } - - function shouldSendBotReply() { - if (commentAuthorIsCloseContributor) { - return [false, null]; - } - - if (issueCreator === commentAuthor) { - return [false, null]; - } - - if (isHelpWanted && isIssueAssignedToSomeoneElse && isAssignmentRequest) { - return [true, BOT_MESSAGE_ALREADY_ASSIGNED]; - } - - if (!isHelpWanted && isAssignmentRequest) { - return [true, BOT_MESSAGE_ISSUE_NOT_OPEN]; - } - - return [false, null]; - } - - function shouldContactSupport() { - if (commentAuthorIsCloseContributor) { - return true; - } - - if (!isHelpWanted) { - return false; - } - - if (isHelpWanted && isIssueAssignedToSomeoneElse) { - return false; - } - - return true; - } - - const [shouldPostBot, botMessage] = shouldSendBotReply(); + const isIssueAssignedToSomeoneElse = + issueAssignees && issueAssignees.length > 0 && !issueAssignees.includes(commentAuthor); + const isHelpWanted = await hasLabel( + ISSUE_LABEL_HELP_WANTED, + owner, + repo, + issueNumber, + github, + core, + ); + const commentAuthorIsCloseContributor = await isCloseContributor(commentAuthor, { + github, + context, + core, + }); + + const [shouldPostBot, botMessage] = shouldSendBotReply( + issueCreator, + commentAuthor, + commentAuthorIsCloseContributor, + isHelpWanted, + isAssignmentRequest, + isIssueAssignedToSomeoneElse, + ); if (shouldPostBot) { // post bot reply only when there are no same bot comments // in the past hour to prevent overwhelming issue comment section - const skipBot = await hasRecentBotComment( - issueNumber, - LE_BOT_USERNAME, - botMessage, - 3600000, - { github, context, core } - ); + const skipBot = await hasRecentBotComment(issueNumber, LE_BOT_USERNAME, botMessage, 3600000, { + github, + context, + core, + }); if (skipBot) { const slackMessage = `*[${repo}] Bot response skipped on issue: <${issueUrl}|${issueTitle}> (less than 1 hour since last bot message)*`; core.setOutput('support_dev_notifications_bot', slackMessage); } else { - const botMessageUrl = await sendBotMessage(issueNumber, botMessage, { github, context, core }); + const botMessageUrl = await sendBotMessage(issueNumber, botMessage, { + github, + context, + core, + }); if (botMessageUrl) { const slackMessage = `*[${repo}] <${botMessageUrl}|Bot response sent> on issue: <${issueUrl}|${issueTitle}>*`; core.setOutput('support_dev_notifications_bot', slackMessage); @@ -166,8 +197,8 @@ module.exports = async ({ github, context, core }) => { if (contactSupport) { const [assignedOpenIssues, openPRs] = await Promise.all([ - getIssues(commentAuthor, 'open'), - getPullRequests(commentAuthor, 'open') + getIssues(commentAuthor, 'open', owner, repo, github, core), + getPullRequests(commentAuthor, 'open', owner, repo, github, core), ]); const authorActivity = formatAuthorActivity(assignedOpenIssues, openPRs); slackMessage += ` _${authorActivity}_`; diff --git a/scripts/contributor-pr-reply.js b/scripts/contributor-pr-reply.js index b35f128..14f3ec8 100644 --- a/scripts/contributor-pr-reply.js +++ b/scripts/contributor-pr-reply.js @@ -8,7 +8,11 @@ module.exports = async ({ github, context, core }) => { const url = context.payload.pull_request.html_url; const title = context.payload.pull_request.title; - const botMessageUrl = await sendBotMessage(number, BOT_MESSAGE_PULL_REQUEST, { github, context, core }); + const botMessageUrl = await sendBotMessage(number, BOT_MESSAGE_PULL_REQUEST, { + github, + context, + core, + }); if (botMessageUrl) { const slackMessage = `*[${repo}] <${botMessageUrl}|Reply sent> on pull request: <${url}|${title}>*`; diff --git a/scripts/get_community_assigned_issues.py b/scripts/get_community_assigned_issues.py index 4e59bf5..9e51dbd 100644 --- a/scripts/get_community_assigned_issues.py +++ b/scripts/get_community_assigned_issues.py @@ -2,7 +2,7 @@ # to external contributors. # This is not used by any workflows, and meant to be used locally # whenever need arises. The script requires `GITHUB_TOKEN` -# environment variable with 'repo' and 'read:org' permissions. +# environment variable with 'repo' and 'read:org' permissions. import os import requests @@ -20,14 +20,14 @@ "Accept": "application/vnd.github+json" } -def fetch_paginated_results(url): - results = [] - while url: - response = requests.get(url, headers=HEADERS) - response.raise_for_status() - results.extend(response.json()) - url = response.links.get("next", {}).get("url") - return results +def fetch_paginated_results(url): + results = [] + while url: + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + results.extend(response.json()) + url = response.links.get("next", {}).get("url") + return results def get_team_members(org): """Fetch all team members for the organization.""" @@ -61,7 +61,7 @@ def main(): issue["html_url"] for issue in filtered_issues ) - + for url in all_filtered_issues: print(url) diff --git a/scripts/holiday-message.js b/scripts/holiday-message.js index 8577996..c9e4abf 100644 --- a/scripts/holiday-message.js +++ b/scripts/holiday-message.js @@ -1,38 +1,30 @@ // See docs/community-automations.md const { HOLIDAY_MESSAGE, LE_BOT_USERNAME } = require('./constants'); -const { - sendBotMessage, - escapeIssueTitleForSlackMessage, - hasRecentBotComment -} = require('./utils'); +const { sendBotMessage, escapeIssueTitleForSlackMessage, hasRecentBotComment } = require('./utils'); module.exports = async ({ github, context, core }) => { try { const repo = context.repo.repo; - + const isPullRequest = !!context.payload.pull_request; const number = isPullRequest ? context.payload.pull_request.number : context.payload.issue.number; - const url = isPullRequest - ? context.payload.pull_request.html_url + const url = isPullRequest + ? context.payload.pull_request.html_url : context.payload.issue.html_url; const title = escapeIssueTitleForSlackMessage( - isPullRequest - ? context.payload.pull_request.title - : context.payload.issue.title + isPullRequest ? context.payload.pull_request.title : context.payload.issue.title, ); // post bot reply only when there are no same bot comments // in the past hour to prevent overwhelming issue comment section - const skipBot = await hasRecentBotComment( - number, - LE_BOT_USERNAME, - HOLIDAY_MESSAGE, - 3600000, - { github, context, core } - ); + const skipBot = await hasRecentBotComment(number, LE_BOT_USERNAME, HOLIDAY_MESSAGE, 3600000, { + github, + context, + core, + }); if (skipBot) { const itemType = isPullRequest ? 'pull request' : 'issue'; const slackBotSkippedMessage = `*[${repo}] Holiday message skipped on ${itemType}: <${url}|${title}> (less than 1 hour since last holiday message)*`; @@ -40,11 +32,7 @@ module.exports = async ({ github, context, core }) => { return; } - const botMessageUrl = await sendBotMessage( - number, - HOLIDAY_MESSAGE, - { github, context, core } - ); + const botMessageUrl = await sendBotMessage(number, HOLIDAY_MESSAGE, { github, context, core }); if (botMessageUrl) { const itemType = isPullRequest ? 'pull request' : 'issue'; diff --git a/scripts/is-contributor.js b/scripts/is-contributor.js index 3bd9754..dc693a4 100644 --- a/scripts/is-contributor.js +++ b/scripts/is-contributor.js @@ -8,7 +8,11 @@ module.exports = async ({ core, github, context }) => { const username = process.env.USERNAME; const authorAssociation = process.env.AUTHOR_ASSOCIATION; - const isUserContributor = await isContributor(username, authorAssociation, { github, context, core }); + const isUserContributor = await isContributor(username, authorAssociation, { + github, + context, + core, + }); core.setOutput('is_contributor', isUserContributor); }; diff --git a/scripts/manage-issue-header.js b/scripts/manage-issue-header.js index d606d57..36dd58d 100644 --- a/scripts/manage-issue-header.js +++ b/scripts/manage-issue-header.js @@ -7,19 +7,24 @@ const HELP_WANTED_LABEL = 'help wanted'; const HEADER_START_MARKER = ''; const HEADER_END_MARKER = ''; -const HELP_WANTED_HEADER = '\n\n\n\n๐Ÿ™‚ Looking for an issue? Welcome! This issue is open for contribution. If this is the first time youโ€™re requesting an issue, please:\n\n- **Read Contributing guidelines** carefully. **Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai)**. **Pull requests and comments that donโ€™t follow the guidelines wonโ€™t be answered.**\n- **Confirm that youโ€™ve read the guidelines** in your comment.\n\n\n\n\n\n'; +const HELP_WANTED_HEADER = + '\n\n\n\n๐Ÿ™‚ Looking for an issue? Welcome! This issue is open for contribution. If this is the first time youโ€™re requesting an issue, please:\n\n- **Read Contributing guidelines** carefully. **Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai)**. **Pull requests and comments that donโ€™t follow the guidelines wonโ€™t be answered.**\n- **Confirm that youโ€™ve read the guidelines** in your comment.\n\n\n\n\n\n'; -const NON_HELP_WANTED_HEADER = '\n\n\n\nโŒ **This issue is not open for contribution. Visit Contributing guidelines** to learn about the contributing process and how to find suitable issues.\n\n\n\n\n\n'; +const NON_HELP_WANTED_HEADER = + '\n\n\n\nโŒ **This issue is not open for contribution. Visit Contributing guidelines** to learn about the contributing process and how to find suitable issues.\n\n\n\n\n\n'; -function clearHeader(issueBody) { +function clearHeader(issueBody) { const startIndex = issueBody.indexOf(HEADER_START_MARKER); const endIndex = issueBody.indexOf(HEADER_END_MARKER); - + if (startIndex === -1 || endIndex === -1) { return issueBody; } - return issueBody.substring(0, startIndex) + issueBody.substring(endIndex + HEADER_END_MARKER.length).trimStart(); + return ( + issueBody.substring(0, startIndex) + + issueBody.substring(endIndex + HEADER_END_MARKER.length).trimStart() + ); } function isIssueHelpWanted(issue) { @@ -38,16 +43,17 @@ module.exports = async ({ github, context, core }) => { const labelName = context.payload.label?.name; let issue = context.payload.issue; let header = ''; - + switch (actionType) { case 'opened': - // also handle pre-existing 'help wanted' label on transferred issues (processed via 'opened' event in a receiving repository) + // also handle pre-existing 'help wanted' label on transferred issues + // (processed via 'opened' event in a receiving repository) header = isIssueHelpWanted(issue) ? HELP_WANTED_HEADER : NON_HELP_WANTED_HEADER; - break; + break; case 'reopened': // check for pre-existing 'help wanted' label header = isIssueHelpWanted(issue) ? HELP_WANTED_HEADER : NON_HELP_WANTED_HEADER; - break; + break; case 'labeled': if (labelName === HELP_WANTED_LABEL) { header = HELP_WANTED_HEADER; @@ -66,10 +72,10 @@ module.exports = async ({ github, context, core }) => { issue = await github.rest.issues.get({ owner: repoOwner, repo: repoName, - issue_number: issueNumber + issue_number: issueNumber, }); - - const currentBody = issue.data.body || ""; + + const currentBody = issue.data.body || ''; let newBody = clearHeader(currentBody); newBody = header + newBody; @@ -78,9 +84,8 @@ module.exports = async ({ github, context, core }) => { owner: repoOwner, repo: repoName, issue_number: issueNumber, - body: newBody - }); - + body: newBody, + }); } catch (error) { core.setFailed(`Error: ${error.message}`); } diff --git a/scripts/unassign-inactive-issues.js b/scripts/unassign-inactive-issues.js index 91f9081..741d5f6 100644 --- a/scripts/unassign-inactive-issues.js +++ b/scripts/unassign-inactive-issues.js @@ -1,8 +1,6 @@ -const fetch = require('node-fetch-native'); - -const formatUnassignments = (unassignments) => { +const formatUnassignments = unassignments => { if (unassignments.length === 0) return ''; - + // Group unassignments by issue const groupedByIssue = unassignments.reduce((acc, curr) => { const key = `${curr.repo}#${curr.issueNumber}`; @@ -12,7 +10,7 @@ const formatUnassignments = (unassignments) => { owner: curr.owner, issueNumber: curr.issueNumber, issueUrl: curr.issueUrl, - users: [] + users: [], }; } acc[key].users.push(curr.user); @@ -21,8 +19,9 @@ const formatUnassignments = (unassignments) => { // Format the grouped unassignments return Object.values(groupedByIssue) - .map(({ users, repo, issueNumber, issueUrl }) => - `${users.map(u => `@${u}`).join(', ')} from <${issueUrl}|${repo}#${issueNumber}>` + .map( + ({ users, repo, issueNumber, issueUrl }) => + `${users.map(u => `@${u}`).join(', ')} from <${issueUrl}|${repo}#${issueNumber}>`, ) .join(', '); }; @@ -31,10 +30,11 @@ async function getAllIssues(github, owner, repo) { const allIssues = []; let page = 1; const perPage = 100; - + + // eslint-disable-next-line no-constant-condition while (true) { //console.log(`Fetching page ${page} of issues...`); - + try { const response = await github.rest.issues.listForRepo({ owner, @@ -43,29 +43,30 @@ async function getAllIssues(github, owner, repo) { per_page: perPage, page: page, filter: 'all', - pulls: false + pulls: false, }); - + const issues = response.data.filter(issue => !issue.pull_request); - + if (issues.length === 0) { break; // No more issues to fetch } - + allIssues.push(...issues); //console.log(`Fetched ${issues.length} issues (excluding PRs) from page ${page}`); - + if (issues.length < perPage) { break; // Last page has fewer items than perPage } - + page++; } catch (error) { + // eslint-disable-next-line no-console console.error(`Error fetching issues page ${page}:`, error); break; } } - + //console.log(`Total issues fetched (excluding PRs): ${allIssues.length}`); return allIssues; } @@ -73,6 +74,7 @@ async function getAllIssues(github, owner, repo) { const checkLinkedPRs = async (issue, github, owner, repo) => { try { if (!issue || !issue.number) { + // eslint-disable-next-line no-console console.error('Invalid issue object received:', issue); return new Set(); // Return empty Set instead of false } @@ -80,38 +82,39 @@ const checkLinkedPRs = async (issue, github, owner, repo) => { let linkedPRs = new Set(); // Method 1: Check timeline with enhanced connected event handling - try { + try { const { data: timelineEvents } = await github.rest.issues.listEventsForTimeline({ owner, repo, issue_number: issue.number, - per_page: 100 + per_page: 100, }); - for (const event of timelineEvents) { if ( - // Check if the event type is 'connected' or 'cross-referenced'. - // These events indicate that GitHub automatically linked the issue to a PR based on commit messages or references. - (event.event === 'connected' || event.event === 'cross-referenced') || - + // Check if the event type is 'connected' or 'cross-referenced'. + // These events indicate that GitHub automatically linked the issue + // to a PR based on commit messages or references. + event.event === 'connected' || + event.event === 'cross-referenced' || // Look for a 'referenced' event that includes a commit ID and a linked PR. // This implies that the commit in a PR mentions the issue, suggesting a connection. - (event.event === 'referenced' && event?.commit_id && event?.source?.issue?.pull_request) || - + (event.event === 'referenced' && + event?.commit_id && + event?.source?.issue?.pull_request) || // Check for a 'closed' event that has an associated commit and a linked PR. // This typically means the issue was closed by a commit referenced in the PR. (event.event === 'closed' && event?.commit_id && event?.source?.issue?.pull_request) || - // Confirm a 'connected' event where the linked PR is not merged yet. // This ensures that we consider PRs that are still open and haven't been merged. (event.event === 'connected' && event?.source?.issue?.pull_request?.merged === false) ) { try { //issue.number is used because every pull request is also an issue. - //Some GitHub events only provide the issue object, so using issue.number ensures consistent access to the PR number across events. + //Some GitHub events only provide the issue object, so using issue.number + // ensures consistent access to the PR number across events. let prNumber = event?.source?.issue?.number; - + if (!prNumber && event?.source?.pull_request?.number) { prNumber = event.source.pull_request.number; } @@ -121,15 +124,16 @@ const checkLinkedPRs = async (issue, github, owner, repo) => { const { data: pr } = await github.rest.pulls.get({ owner, repo, - pull_number: prNumber + pull_number: prNumber, }); - + if (pr && pr.state === 'open') { //console.log(`Found valid linked PR #${prNumber} (${pr.state})`); - linkedPRs.add(prNumber); + linkedPRs.add(prNumber); } - }else{ - // Fallback for PRs linked via GitHub UI where PR number cannot be retrieved from the payload + } else { + // Fallback for PRs linked via GitHub UI where PR number + // cannot be retrieved from the payload //console.log('found pr linked in the issue'); linkedPRs.add(1); // Adds a placeholder to indicate a linked PR was found } @@ -139,27 +143,28 @@ const checkLinkedPRs = async (issue, github, owner, repo) => { } } } catch (timelineError) { + // eslint-disable-next-line no-console console.error(`Error fetching timeline for issue #${issue.number}:`, timelineError.message); } // Method 2: Search for PRs that mention this issue using the updated endpoint try { const searchQuery = `repo:${owner}/${repo} type:pr is:open ${issue.number} in:body,title`; - + const searchResult = await github.request('GET /search/issues', { q: searchQuery, advanced_search: true, // Enable advanced search headers: { - 'Accept': 'application/vnd.github+json', - } + Accept: 'application/vnd.github+json', + }, }); - + // Local regex for "closes/fixes/resolves #123" const closingRegex = new RegExp( `(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\\s*:?\\s*#${issue.number}`, - "i" + 'i', ); - + for (const prItem of searchResult.data.items || []) { if (prItem.pull_request && prItem.number) { const prDetails = await github.rest.pulls.get({ @@ -167,10 +172,10 @@ const checkLinkedPRs = async (issue, github, owner, repo) => { repo, pull_number: prItem.number, }); - - if (prDetails.data.state === "open") { - const prBody = prDetails.data.body || ""; - const prTitle = prDetails.data.title || ""; + + if (prDetails.data.state === 'open') { + const prBody = prDetails.data.body || ''; + const prTitle = prDetails.data.title || ''; if (closingRegex.test(prBody) || closingRegex.test(prTitle)) { linkedPRs.add(prItem.number); } @@ -179,10 +184,11 @@ const checkLinkedPRs = async (issue, github, owner, repo) => { } } catch (searchError) { //console.log('Search API error:', searchError.message); - } + } // Return the Set of linked PR numbers (always return a Set) return linkedPRs; } catch (error) { + // eslint-disable-next-line no-console console.error(`Error in checkLinkedPRs for issue #${issue.number}:`, error); return new Set(); // Return empty Set instead of false } @@ -194,7 +200,7 @@ const checkUserMembership = async (owner, repo, username, github) => { // Check if the user is an owner of the repository const repoDetails = await github.rest.repos.get({ owner, - repo + repo, }); // Check if the repository owner matches the username @@ -207,7 +213,7 @@ const checkUserMembership = async (owner, repo, username, github) => { try { await github.rest.orgs.getMembershipForUser({ org: owner, - username: username + username: username, }); //console.log(`${username} is an organization member`); return true; @@ -216,6 +222,7 @@ const checkUserMembership = async (owner, repo, username, github) => { return false; } } catch (error) { + // eslint-disable-next-line no-console console.error(`Error checking user membership for ${username}:`, error); return false; } @@ -223,36 +230,38 @@ const checkUserMembership = async (owner, repo, username, github) => { module.exports = async ({ github, context, core }) => { try { - const unassignments = []; const inactivityThresholdMs = 30 * 24 * 60 * 60 * 1000; // for 1 month const [owner, repo] = context.payload.repository.full_name.split('/'); //console.log(`Processing repository: ${owner}/${repo}`); - + try { // Test API access by getting repository details - const { data: repository } = await github.rest.repos.get({ + await github.rest.repos.get({ owner, - repo + repo, }); // console.log('Successfully authenticated with GitHub App and verified repository access'); // console.log(`Repository: ${repository.full_name}`); } catch (authError) { + // eslint-disable-next-line no-console console.error('Authentication error details:', { message: authError.message, status: authError.status, - documentation_url: authError.documentation_url + documentation_url: authError.documentation_url, }); - throw new Error(`Repository access failed. Please check your GitHub App permissions for repository access. Error: ${authError.message}`); + throw new Error( + `Repository access failed. Please check your GitHub App permissions for repository access. Error: ${authError.message}`, + ); } - const issues = await getAllIssues(github, owner, repo); //console.log(`Processing ${issues.length} open issues`); for (const issue of issues) { if (!issue || !issue.number) { + // eslint-disable-next-line no-console console.error('Skipping invalid issue:', issue); continue; } @@ -274,7 +283,7 @@ module.exports = async ({ github, context, core }) => { // Check if issue is inactive const lastActivity = new Date(issue.updated_at); const now = new Date(); - + if (now - lastActivity <= inactivityThresholdMs) { //console.log(`Issue #${issue.number} is still active, skipping`); continue; @@ -282,7 +291,7 @@ module.exports = async ({ github, context, core }) => { //console.log(`Checking for linked PRs for issue #${issue.number}`); const hasOpenPRs = await checkLinkedPRs(issue, github, owner, repo); - + if (hasOpenPRs.size > 0) { //console.log(`Issue #${issue.number} has open PRs, skipping unassignment`); continue; @@ -299,7 +308,10 @@ module.exports = async ({ github, context, core }) => { continue; } - if (assignee.site_admin || await checkUserMembership(owner, repo, assignee.login, github)) { + if ( + assignee.site_admin || + (await checkUserMembership(owner, repo, assignee.login, github)) + ) { activeAssignees.push(assignee.login); //console.log(`${assignee.login} is an active member, keeping assignment`); } else { @@ -340,36 +352,40 @@ module.exports = async ({ github, context, core }) => { repo, owner, issueNumber: issue.number, - issueUrl: `https://github.com/${owner}/${repo}/issues/${issue.number}` + issueUrl: `https://github.com/${owner}/${repo}/issues/${issue.number}`, }); }); - } catch (issueError) { + // eslint-disable-next-line no-console console.error(`Error processing issue #${issue.number}:`, { message: issueError.message, - status: issueError.status + status: issueError.status, }); if (issueError.status === 403) { - throw new Error('Token lacks necessary permissions. Ensure it has "issues" and "write" access.'); + throw new Error( + 'Token lacks necessary permissions. Ensure it has "issues" and "write" access.', + ); } } } const formattedUnassignments = formatUnassignments(unassignments); //console.log('Unassignments completed:', unassignments.length); - + try { core.setOutput('unassignments', formattedUnassignments); return formattedUnassignments; } catch (error) { + // eslint-disable-next-line no-console console.error('Error setting output:', error); core.setFailed(error.message); } - } catch (error) { + // eslint-disable-next-line no-console console.error('Action failed:', error); + // eslint-disable-next-line no-console console.error('Full error details:', error); core.setFailed(error.message); return ''; } -}; \ No newline at end of file +}; diff --git a/scripts/update-pr-spreadsheet.js b/scripts/update-pr-spreadsheet.js index a637538..5c2db3c 100644 --- a/scripts/update-pr-spreadsheet.js +++ b/scripts/update-pr-spreadsheet.js @@ -1,7 +1,7 @@ const { google } = require('googleapis'); // Extract relevant data from the event payload -const extractPRData = (payload) => { +const extractPRData = payload => { const pr = payload.pull_request; return { merged_at: pr.merged_at, @@ -15,7 +15,7 @@ const extractPRData = (payload) => { user_site_admin: pr.user.site_admin, user_type: pr.user.type, author_association: pr.author_association, - state: pr.state + state: pr.state, }; }; @@ -25,14 +25,12 @@ async function authorize(googleCredentials) { const credentials = JSON.parse(googleCredentials); const auth = new google.auth.GoogleAuth({ credentials, - scopes: ["https://www.googleapis.com/auth/spreadsheets"], + scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); const authClient = await auth.getClient(); - return google.sheets({ version: "v4", auth: authClient }); + return google.sheets({ version: 'v4', auth: authClient }); } catch (error) { - throw new Error( - `Failed to authorize: ${error.message}.` - ); + throw new Error(`Failed to authorize: ${error.message}.`); } } @@ -40,16 +38,18 @@ async function authorize(googleCredentials) { async function updateSpreadsheet(pullRequest, sheetId, sheetName, googleCredentials) { const sheets = await authorize(googleCredentials); const prData = [ - pullRequest.merged_at ? pullRequest.merged_at.split("T")[0].replace("'", "") : pullRequest.state === "closed" ? "closed" : pullRequest.state, - pullRequest.html_url || "", - pullRequest.user_login || "", - pullRequest.title || "", - pullRequest.repo_name || "", - pullRequest.created_at - ? pullRequest.created_at.split("T")[0].replace("'", "") - : "", - pullRequest.requested_reviewers || "", - pullRequest.assignees || "", + pullRequest.merged_at + ? pullRequest.merged_at.split('T')[0].replace("'", '') + : pullRequest.state === 'closed' + ? 'closed' + : pullRequest.state, + pullRequest.html_url || '', + pullRequest.user_login || '', + pullRequest.title || '', + pullRequest.repo_name || '', + pullRequest.created_at ? pullRequest.created_at.split('T')[0].replace("'", '') : '', + pullRequest.requested_reviewers || '', + pullRequest.assignees || '', ]; try { @@ -77,10 +77,10 @@ async function updateSpreadsheet(pullRequest, sheetId, sheetName, googleCredenti const existingDataString = JSON.stringify(existingData); if (prDataString !== existingDataString) { - console.log(`Detected changes for row ${rowToUpdate}.`); + // console.log(`Detected changes for row ${rowToUpdate}.`); const updates = []; - const columns = ["A", "B", "C", "D", "E", "F", "G", "H"]; + const columns = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; for (let col = 0; col < prData.length; col++) { if (existingData[col] !== prData[col]) { updates.push({ @@ -96,32 +96,32 @@ async function updateSpreadsheet(pullRequest, sheetId, sheetName, googleCredenti spreadsheetId: sheetId, resource: { data: updates, - valueInputOption: "RAW", + valueInputOption: 'RAW', }, }); - console.log(`Updated row ${rowToUpdate} in Google Sheets.`); + // console.log(`Updated row ${rowToUpdate} in Google Sheets.`); } else { - console.log(`No changes detected for row ${rowToUpdate}.`); + // console.log(`No changes detected for row ${rowToUpdate}.`); } } else { - console.log(`No changes detected for row ${rowToUpdate}.`); + // console.log(`No changes detected for row ${rowToUpdate}.`); } } else { // Append new row starting from column B await sheets.spreadsheets.values.append({ spreadsheetId: sheetId, range: `${sheetName}!A:H`, - valueInputOption: "RAW", + valueInputOption: 'RAW', resource: { values: [prData] }, }); - console.log(`Added new row to Google Sheets.`); + // console.log(`Added new row to Google Sheets.`); } } catch (error) { throw new Error(`Failed to update spreadsheet: ${error.message}`); } } -module.exports = async ({ github, context, core }) => { +module.exports = async ({ context, core }) => { const sheetId = process.env.CONTRIBUTIONS_SPREADSHEET_ID; const sheetName = process.env.CONTRIBUTIONS_SHEET_NAME; const googleCredentials = process.env.GH_UPLOADER_GCP_SA_CREDENTIALS; diff --git a/scripts/utils.js b/scripts/utils.js index 58bb619..2017d64 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,5 +1,11 @@ -const { LE_BOT_USERNAME, SENTRY_BOT_USERNAME, HOLIDAY_MESSAGE_START_DATE, HOLIDAY_MESSAGE_END_DATE } = require('./constants'); -const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); +const { + LE_BOT_USERNAME, + SENTRY_BOT_USERNAME, + HOLIDAY_MESSAGE_START_DATE, + HOLIDAY_MESSAGE_END_DATE, +} = require('./constants'); +// const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); +const { CLOSE_CONTRIBUTORS } = require('./constants'); /** * Checks if username belongs to one of our bots. @@ -49,7 +55,7 @@ async function isContributor(username, authorAssociation, { github, context, cor * Checks if a user is a close contributor by checking * both the constants list and team membership in monitored teams. */ -async function isCloseContributor(username, { github, context, core }) { +async function isCloseContributor(username, { core }) { if (!username) { core.setFailed('Missing username'); return false; @@ -112,7 +118,7 @@ function isHolidayMessageActive(currentDate = new Date()) { /** * Sends a bot message as a comment on an issue. Returns message URL if successful. */ -async function sendBotMessage(issueNumber, message, { github, context, core }) { +async function sendBotMessage(issueNumber, message, { github, context }) { try { if (!issueNumber) { throw new Error('Issue number is required'); @@ -146,7 +152,13 @@ function escapeIssueTitleForSlackMessage(issueTitle) { * Checks if a bot sent a message with a given text on an issue * in the past specified milliseconds. */ -async function hasRecentBotComment(issueNumber, botUsername, commentText, msAgo, { github, context, core }) { +async function hasRecentBotComment( + issueNumber, + botUsername, + commentText, + msAgo, + { github, context, core }, +) { const oneHourAgo = new Date(Date.now() - msAgo); const owner = context.repo.owner; const repo = context.repo.repo; @@ -156,9 +168,12 @@ async function hasRecentBotComment(issueNumber, botUsername, commentText, msAgo, owner, repo, issue_number: issueNumber, - since: oneHourAgo.toISOString() + since: oneHourAgo.toISOString(), }); - return (response.data || []).some(comment => comment.user && comment.user.login === botUsername && comment.body.includes(commentText)); + return (response.data || []).some( + comment => + comment.user && comment.user.login === botUsername && comment.body.includes(commentText), + ); } catch (error) { core.warning(`Failed to fetch comments on issue #${issueNumber}: ${error.message}`); } From 18a9ee58a9d2cbb79f395f1a7a253045e16b090c Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 13:36:32 +0100 Subject: [PATCH 24/35] Update message --- scripts/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/constants.js b/scripts/constants.js index f044af1..ffe089d 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -77,7 +77,7 @@ const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your intere const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; -const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure:\n\n- [ ] **Linting and tests pass**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; +const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure that:\n\n- [ ] **You ran \`pre-commit\` locally**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; // Holiday message will be sent between the following datetimes // Additionally before/after holidays, enable/disable all related workflows From ba8dd2212d7113cf6a34e87b03564aed4419c2a6 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 13:41:49 +0100 Subject: [PATCH 25/35] Remove holiday message date restriction in favor of manual enable/disable (to prevent from forgetting to enable/disable the workflow and having it run many times unnecessarily) --- .github/workflows/holiday-message.yml | 12 ++----- .github/workflows/is-holiday-active.yml | 46 ------------------------- docs/community-automations.md | 4 +-- scripts/constants.js | 8 ----- scripts/utils.js | 12 +------ 5 files changed, 4 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/is-holiday-active.yml diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 7a8314b..5faa2a5 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -12,16 +12,8 @@ on: required: true description: 'Webhook URL for Slack #support-dev-notifications channel' jobs: - check-holiday-active: - name: Check if holiday message is active - uses: learningequality/.github/.github/workflows/is-holiday-active.yml@main - secrets: - LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} - LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} check-if-contributor: name: Check if author is contributor - needs: [check-holiday-active] - if: ${{ needs.check-holiday-active.outputs.is_holiday_active == 'true' }} uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} @@ -31,8 +23,8 @@ jobs: author_association: ${{ github.event_name == 'pull_request' && github.event.pull_request.author_association || github.event_name == 'issue_comment' && github.event.comment.author_association }} post-holiday-message: name: Post holiday message - needs: [check-holiday-active, check-if-contributor] - if: ${{ needs.check-holiday-active.outputs.is_holiday_active == 'true' && needs.check-if-contributor.outputs.is_contributor == 'true' }} + needs: [check-if-contributor] + if: ${{ needs.check-if-contributor.outputs.is_contributor == 'true' }} runs-on: ubuntu-latest steps: - name: Generate App Token diff --git a/.github/workflows/is-holiday-active.yml b/.github/workflows/is-holiday-active.yml deleted file mode 100644 index 0a53c4d..0000000 --- a/.github/workflows/is-holiday-active.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Check if holiday message is active -on: - workflow_call: - secrets: - LE_BOT_APP_ID: - description: 'GitHub App ID for authentication' - required: true - LE_BOT_PRIVATE_KEY: - description: 'GitHub App Private Key for authentication' - required: true - outputs: - is_holiday_active: - description: "True if we're within the holiday message period" - value: ${{ jobs.check-holiday-active.outputs.is_holiday_active }} -jobs: - check-holiday-active: - name: Check if holiday message is active - runs-on: ubuntu-latest - outputs: - is_holiday_active: ${{ steps.script.outputs.is_holiday_active }} - steps: - - name: Generate App Token - id: generate-token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.LE_BOT_APP_ID }} - private_key: ${{ secrets.LE_BOT_PRIVATE_KEY }} - - name: Checkout .github repository - uses: actions/checkout@v4 - with: - repository: learningequality/.github - ref: main - token: ${{ steps.generate-token.outputs.token }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Run script - id: script - uses: actions/github-script@v7 - with: - script: | - const { isHolidayMessageActive } = require('./scripts/utils'); - core.setOutput('is_holiday_active', isHolidayMessageActive()); diff --git a/docs/community-automations.md b/docs/community-automations.md index 13041d5..2a51041 100644 --- a/docs/community-automations.md +++ b/docs/community-automations.md @@ -33,8 +33,6 @@ In `scripts/contants.js` set: Sends a holiday message to community pull requests and issue comments. In `scripts/contants.js` set: - - `HOLIDAY_MESSAGE`: Message text -- `HOLIDAY_MESSAGE_START_DATE` and `HOLIDAY_MESSAGE_END_DATE`: From and till when the message should be sent -Additionally before/after holidays, enable/disable all related workflows in all repositories that use it (search for `call-holiday-message`). +Before/after holidays, enable/disable all related workflows in all repositories that use it (search for `call-holiday-message`). diff --git a/scripts/constants.js b/scripts/constants.js index ffe089d..e8e1f32 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -79,12 +79,6 @@ const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your inte const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure that:\n\n- [ ] **You ran \`pre-commit\` locally**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; -// Holiday message will be sent between the following datetimes -// Additionally before/after holidays, enable/disable all related workflows -// in all repositories that use it (search for `call-holiday-message`). -const HOLIDAY_MESSAGE_START_DATE = new Date('2025-12-15T00:00:00Z'); -const HOLIDAY_MESSAGE_END_DATE = new Date('2026-01-05T23:59:59Z'); - const HOLIDAY_MESSAGE = `Seasonโ€™s greetings! ๐Ÿ‘‹ \n\n Weโ€™d like to thank everyone for another year of fruitful collaborations, engaging discussions, and for the continued support of our work. **Learning Equality will be on holidays from December 22 to January 5.** We look forward to much more in the new year and wish you a very happy holiday season!`; module.exports = { @@ -97,7 +91,5 @@ module.exports = { BOT_MESSAGE_ALREADY_ASSIGNED, BOT_MESSAGE_PULL_REQUEST, TEAMS_WITH_CLOSE_CONTRIBUTORS, - HOLIDAY_MESSAGE_START_DATE, - HOLIDAY_MESSAGE_END_DATE, HOLIDAY_MESSAGE, }; diff --git a/scripts/utils.js b/scripts/utils.js index 2017d64..821bbcc 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,9 +1,4 @@ -const { - LE_BOT_USERNAME, - SENTRY_BOT_USERNAME, - HOLIDAY_MESSAGE_START_DATE, - HOLIDAY_MESSAGE_END_DATE, -} = require('./constants'); +const { LE_BOT_USERNAME, SENTRY_BOT_USERNAME } = require('./constants'); // const { CLOSE_CONTRIBUTORS, TEAMS_WITH_CLOSE_CONTRIBUTORS } = require('./constants'); const { CLOSE_CONTRIBUTORS } = require('./constants'); @@ -111,10 +106,6 @@ async function isCloseContributor(username, { core }) { } */ } -function isHolidayMessageActive(currentDate = new Date()) { - return currentDate >= HOLIDAY_MESSAGE_START_DATE && currentDate <= HOLIDAY_MESSAGE_END_DATE; -} - /** * Sends a bot message as a comment on an issue. Returns message URL if successful. */ @@ -183,7 +174,6 @@ module.exports = { isContributor, isCloseContributor, isBot, - isHolidayMessageActive, sendBotMessage, escapeIssueTitleForSlackMessage, hasRecentBotComment, From 70393f09f274aa83cd92b0e1fd25b80b542c65d6 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 14:03:29 +0100 Subject: [PATCH 26/35] Temporarily change branch reference for testing --- .github/workflows/community-contribution-label.yml | 3 ++- .github/workflows/contributor-issue-comment.yml | 6 +++--- .github/workflows/contributor-pr-reply.yml | 6 +++--- .github/workflows/holiday-message.yml | 6 +++--- .github/workflows/is-contributor.yml | 4 ++-- .github/workflows/manage-issue-header.yml | 4 ++-- .github/workflows/unassign-inactive-issues.yaml | 4 ++-- .github/workflows/update-pr-spreadsheet.yml | 6 +++--- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/community-contribution-label.yml b/.github/workflows/community-contribution-label.yml index f410caa..7ba6858 100644 --- a/.github/workflows/community-contribution-label.yml +++ b/.github/workflows/community-contribution-label.yml @@ -21,7 +21,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - repository: learningequality/.github + repository: MisRob/.github + ref: community-automations-updates path: .github-repo - name: Generate App Token id: generate-token diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index 4e182c5..a1faf05 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -39,8 +39,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml index 48df6a1..f452c42 100644 --- a/.github/workflows/contributor-pr-reply.yml +++ b/.github/workflows/contributor-pr-reply.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 5faa2a5..8f2421e 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/is-contributor.yml b/.github/workflows/is-contributor.yml index 4621daa..d690ea6 100644 --- a/.github/workflows/is-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -35,8 +35,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/manage-issue-header.yml b/.github/workflows/manage-issue-header.yml index 7411b2a..ea02259 100644 --- a/.github/workflows/manage-issue-header.yml +++ b/.github/workflows/manage-issue-header.yml @@ -26,8 +26,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/unassign-inactive-issues.yaml b/.github/workflows/unassign-inactive-issues.yaml index 0093279..3ee7880 100644 --- a/.github/workflows/unassign-inactive-issues.yaml +++ b/.github/workflows/unassign-inactive-issues.yaml @@ -25,8 +25,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index f03ecc5..a1a610f 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -38,8 +38,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 From c20b4ebe5bc8f416d8ce9d53f02c504b4b3d4a9f Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 14:19:31 +0100 Subject: [PATCH 27/35] Fix missing parameters --- scripts/contributor-issue-comment.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/contributor-issue-comment.js b/scripts/contributor-issue-comment.js index 47c3ed8..399ac7c 100644 --- a/scripts/contributor-issue-comment.js +++ b/scripts/contributor-issue-comment.js @@ -192,7 +192,11 @@ module.exports = async ({ github, context, core }) => { } } - const contactSupport = shouldContactSupport(); + const contactSupport = shouldContactSupport( + commentAuthorIsCloseContributor, + isHelpWanted, + isIssueAssignedToSomeoneElse, + ); let slackMessage = `*[${repo}] <${issueUrl}#issuecomment-${commentId}|New comment> on issue: <${issueUrl}|${issueTitle}> by _${commentAuthor}_*`; if (contactSupport) { From 0fefeefee435a78e07199b240593221f65851229 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 17:45:50 +0100 Subject: [PATCH 28/35] Fix condition --- .github/workflows/holiday-message.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 8f2421e..37922fb 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -19,8 +19,8 @@ jobs: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} with: - username: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login || github.event_name == 'issue_comment' && github.event.comment.user.login }} - author_association: ${{ github.event_name == 'pull_request' && github.event.pull_request.author_association || github.event_name == 'issue_comment' && github.event.comment.author_association }} + username: ${{ github.event.pull_request.user.login || github.event.comment.user.login }} + author_association: ${{ github.event.pull_request.author_association || github.event.comment.author_association }} post-holiday-message: name: Post holiday message needs: [check-if-contributor] From 784d98e5888d5e0467cbe2ba0c240edd9f97c1e3 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 17:54:21 +0100 Subject: [PATCH 29/35] Fix variable mismatch --- scripts/update-pr-spreadsheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-pr-spreadsheet.js b/scripts/update-pr-spreadsheet.js index 5c2db3c..f21add2 100644 --- a/scripts/update-pr-spreadsheet.js +++ b/scripts/update-pr-spreadsheet.js @@ -124,7 +124,7 @@ async function updateSpreadsheet(pullRequest, sheetId, sheetName, googleCredenti module.exports = async ({ context, core }) => { const sheetId = process.env.CONTRIBUTIONS_SPREADSHEET_ID; const sheetName = process.env.CONTRIBUTIONS_SHEET_NAME; - const googleCredentials = process.env.GH_UPLOADER_GCP_SA_CREDENTIALS; + const googleCredentials = process.env.GOOGLE_CREDENTIALS; try { const prData = extractPRData(context.payload); From 437211db6f4854dff72f96b5d1dbbb15a3b1c706 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:00:16 +0100 Subject: [PATCH 30/35] Revert "Temporarily change branch reference for testing" This reverts commit 70393f09f274aa83cd92b0e1fd25b80b542c65d6. --- .github/workflows/community-contribution-label.yml | 3 +-- .github/workflows/contributor-issue-comment.yml | 6 +++--- .github/workflows/contributor-pr-reply.yml | 6 +++--- .github/workflows/holiday-message.yml | 6 +++--- .github/workflows/is-contributor.yml | 4 ++-- .github/workflows/manage-issue-header.yml | 4 ++-- .github/workflows/unassign-inactive-issues.yaml | 4 ++-- .github/workflows/update-pr-spreadsheet.yml | 6 +++--- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/community-contribution-label.yml b/.github/workflows/community-contribution-label.yml index 7ba6858..f410caa 100644 --- a/.github/workflows/community-contribution-label.yml +++ b/.github/workflows/community-contribution-label.yml @@ -21,8 +21,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github path: .github-repo - name: Generate App Token id: generate-token diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index a1faf05..4e182c5 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -39,8 +39,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml index f452c42..48df6a1 100644 --- a/.github/workflows/contributor-pr-reply.yml +++ b/.github/workflows/contributor-pr-reply.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 37922fb..8a67a2f 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/is-contributor.yml b/.github/workflows/is-contributor.yml index d690ea6..4621daa 100644 --- a/.github/workflows/is-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -35,8 +35,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/manage-issue-header.yml b/.github/workflows/manage-issue-header.yml index ea02259..7411b2a 100644 --- a/.github/workflows/manage-issue-header.yml +++ b/.github/workflows/manage-issue-header.yml @@ -26,8 +26,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/unassign-inactive-issues.yaml b/.github/workflows/unassign-inactive-issues.yaml index 3ee7880..0093279 100644 --- a/.github/workflows/unassign-inactive-issues.yaml +++ b/.github/workflows/unassign-inactive-issues.yaml @@ -25,8 +25,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index a1a610f..f03ecc5 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -38,8 +38,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 From 57288e4de14bd6b547c8ad99b23a1792c27ccee4 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:50:31 +0100 Subject: [PATCH 31/35] Add GSoC note --- scripts/constants.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/constants.js b/scripts/constants.js index e8e1f32..118df26 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -73,13 +73,17 @@ const KEYWORDS_DETECT_ASSIGNMENT_REQUEST = [ const ISSUE_LABEL_HELP_WANTED = 'help wanted'; -const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; +// Will be attached to bot messages when not empty +// const GSOC_NOTE = ''; +const GSOC_NOTE = `\n\n_Are you preparing for Google Summer of Code? See our [GSoC guidelines.](https://learningequality.org/contributing-to-our-open-code-base/#google-summer-of-code)._`; + +const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š${GSOC_NOTE}`; const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure that:\n\n- [ ] **You ran \`pre-commit\` locally**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; -const HOLIDAY_MESSAGE = `Seasonโ€™s greetings! ๐Ÿ‘‹ \n\n Weโ€™d like to thank everyone for another year of fruitful collaborations, engaging discussions, and for the continued support of our work. **Learning Equality will be on holidays from December 22 to January 5.** We look forward to much more in the new year and wish you a very happy holiday season!`; +const HOLIDAY_MESSAGE = `Seasonโ€™s greetings! ๐Ÿ‘‹ \n\n Weโ€™d like to thank everyone for another year of fruitful collaborations, engaging discussions, and for the continued support of our work. **Learning Equality will be on holidays from December 22 to January 5.** We look forward to much more in the new year and wish you a very happy holiday season!${GSOC_NOTE}`; module.exports = { LE_BOT_USERNAME, From f069c30a4712b981f333e6a2925f79560749050a Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:50:57 +0100 Subject: [PATCH 32/35] Reapply "Temporarily change branch reference for testing" This reverts commit 437211db6f4854dff72f96b5d1dbbb15a3b1c706. --- .github/workflows/community-contribution-label.yml | 3 ++- .github/workflows/contributor-issue-comment.yml | 6 +++--- .github/workflows/contributor-pr-reply.yml | 6 +++--- .github/workflows/holiday-message.yml | 6 +++--- .github/workflows/is-contributor.yml | 4 ++-- .github/workflows/manage-issue-header.yml | 4 ++-- .github/workflows/unassign-inactive-issues.yaml | 4 ++-- .github/workflows/update-pr-spreadsheet.yml | 6 +++--- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/community-contribution-label.yml b/.github/workflows/community-contribution-label.yml index f410caa..7ba6858 100644 --- a/.github/workflows/community-contribution-label.yml +++ b/.github/workflows/community-contribution-label.yml @@ -21,7 +21,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - repository: learningequality/.github + repository: MisRob/.github + ref: community-automations-updates path: .github-repo - name: Generate App Token id: generate-token diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index 4e182c5..a1faf05 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -39,8 +39,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml index 48df6a1..f452c42 100644 --- a/.github/workflows/contributor-pr-reply.yml +++ b/.github/workflows/contributor-pr-reply.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 8a67a2f..37922fb 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/is-contributor.yml b/.github/workflows/is-contributor.yml index 4621daa..d690ea6 100644 --- a/.github/workflows/is-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -35,8 +35,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/manage-issue-header.yml b/.github/workflows/manage-issue-header.yml index 7411b2a..ea02259 100644 --- a/.github/workflows/manage-issue-header.yml +++ b/.github/workflows/manage-issue-header.yml @@ -26,8 +26,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/unassign-inactive-issues.yaml b/.github/workflows/unassign-inactive-issues.yaml index 0093279..3ee7880 100644 --- a/.github/workflows/unassign-inactive-issues.yaml +++ b/.github/workflows/unassign-inactive-issues.yaml @@ -25,8 +25,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index f03ecc5..a1a610f 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: learningequality/.github/.github/workflows/is-contributor.yml@main + uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -38,8 +38,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: learningequality/.github - ref: main + repository: MisRob/.github + ref: community-automations-updates token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 From 23ec6613564864f8ee7b94598d36f87d3f4c1306 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:53:53 +0100 Subject: [PATCH 33/35] Bold format --- scripts/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/constants.js b/scripts/constants.js index 118df26..b55591e 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -75,7 +75,7 @@ const ISSUE_LABEL_HELP_WANTED = 'help wanted'; // Will be attached to bot messages when not empty // const GSOC_NOTE = ''; -const GSOC_NOTE = `\n\n_Are you preparing for Google Summer of Code? See our [GSoC guidelines.](https://learningequality.org/contributing-to-our-open-code-base/#google-summer-of-code)._`; +const GSOC_NOTE = `\n\n**Are you preparing for Google Summer of Code? See our [GSoC guidelines.](https://learningequality.org/contributing-to-our-open-code-base/#google-summer-of-code).**`; const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š${GSOC_NOTE}`; From 39479e5f57ed734315ba5147677399fb6e0c7443 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:57:11 +0100 Subject: [PATCH 34/35] Fix format --- scripts/constants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/constants.js b/scripts/constants.js index b55591e..5e6e6d0 100644 --- a/scripts/constants.js +++ b/scripts/constants.js @@ -75,11 +75,11 @@ const ISSUE_LABEL_HELP_WANTED = 'help wanted'; // Will be attached to bot messages when not empty // const GSOC_NOTE = ''; -const GSOC_NOTE = `\n\n**Are you preparing for Google Summer of Code? See our [GSoC guidelines.](https://learningequality.org/contributing-to-our-open-code-base/#google-summer-of-code).**`; +const GSOC_NOTE = `\n\n**Are you preparing for Google Summer of Code? See our [GSoC guidelines.](https://learningequality.org/contributing-to-our-open-code-base/#google-summer-of-code)**`; const BOT_MESSAGE_ISSUE_NOT_OPEN = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is not open for contribution. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š${GSOC_NOTE}`; -const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š`; +const BOT_MESSAGE_ALREADY_ASSIGNED = `Hi! ๐Ÿ‘‹ \n\n Thanks so much for your interest! **This issue is already assigned. Visit [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base) to learn about the contributing process and how to find suitable issues.** \n\n We really appreciate your willingness to helpโ€”you're welcome to find a more suitable issue, and let us know if you have any questions. ๐Ÿ˜Š${GSOC_NOTE}`; const BOT_MESSAGE_PULL_REQUEST = `๐Ÿ‘‹ Thanks for contributing! \n\n We will assign a reviewer within the next two weeks. In the meantime, please ensure that:\n\n- [ ] **You ran \`pre-commit\` locally**\n- [ ] **All issue requirements are satisfied**\n- [ ] **The contribution is aligned with our [Contributing guidelines](https://learningequality.org/contributing-to-our-open-code-base). Pay extra attention to [Using generative AI](https://learningequality.org/contributing-to-our-open-code-base/#using-generative-ai). Pull requests that don't follow the guidelines will be closed.**\n\nWe'll be in touch! ๐Ÿ˜Š`; From 9b8579a2dc2eed2129bda0a762cc715e6100d898 Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Thu, 4 Dec 2025 18:57:55 +0100 Subject: [PATCH 35/35] Revert "Temporarily change branch reference for testing" This reverts commit f069c30a4712b981f333e6a2925f79560749050a. --- .github/workflows/community-contribution-label.yml | 3 +-- .github/workflows/contributor-issue-comment.yml | 6 +++--- .github/workflows/contributor-pr-reply.yml | 6 +++--- .github/workflows/holiday-message.yml | 6 +++--- .github/workflows/is-contributor.yml | 4 ++-- .github/workflows/manage-issue-header.yml | 4 ++-- .github/workflows/unassign-inactive-issues.yaml | 4 ++-- .github/workflows/update-pr-spreadsheet.yml | 6 +++--- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/community-contribution-label.yml b/.github/workflows/community-contribution-label.yml index 7ba6858..f410caa 100644 --- a/.github/workflows/community-contribution-label.yml +++ b/.github/workflows/community-contribution-label.yml @@ -21,8 +21,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github path: .github-repo - name: Generate App Token id: generate-token diff --git a/.github/workflows/contributor-issue-comment.yml b/.github/workflows/contributor-issue-comment.yml index a1faf05..4e182c5 100644 --- a/.github/workflows/contributor-issue-comment.yml +++ b/.github/workflows/contributor-issue-comment.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -39,8 +39,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/contributor-pr-reply.yml b/.github/workflows/contributor-pr-reply.yml index f452c42..48df6a1 100644 --- a/.github/workflows/contributor-pr-reply.yml +++ b/.github/workflows/contributor-pr-reply.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/holiday-message.yml b/.github/workflows/holiday-message.yml index 37922fb..8a67a2f 100644 --- a/.github/workflows/holiday-message.yml +++ b/.github/workflows/holiday-message.yml @@ -14,7 +14,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -36,8 +36,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/is-contributor.yml b/.github/workflows/is-contributor.yml index d690ea6..4621daa 100644 --- a/.github/workflows/is-contributor.yml +++ b/.github/workflows/is-contributor.yml @@ -35,8 +35,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/manage-issue-header.yml b/.github/workflows/manage-issue-header.yml index ea02259..7411b2a 100644 --- a/.github/workflows/manage-issue-header.yml +++ b/.github/workflows/manage-issue-header.yml @@ -26,8 +26,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/unassign-inactive-issues.yaml b/.github/workflows/unassign-inactive-issues.yaml index 3ee7880..0093279 100644 --- a/.github/workflows/unassign-inactive-issues.yaml +++ b/.github/workflows/unassign-inactive-issues.yaml @@ -25,8 +25,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/update-pr-spreadsheet.yml b/.github/workflows/update-pr-spreadsheet.yml index a1a610f..f03ecc5 100644 --- a/.github/workflows/update-pr-spreadsheet.yml +++ b/.github/workflows/update-pr-spreadsheet.yml @@ -17,7 +17,7 @@ on: jobs: check-if-contributor: name: Check if author is contributor - uses: MisRob/.github/.github/workflows/is-contributor.yml@community-automations-updates + uses: learningequality/.github/.github/workflows/is-contributor.yml@main secrets: LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }} LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }} @@ -38,8 +38,8 @@ jobs: - name: Checkout .github repository uses: actions/checkout@v4 with: - repository: MisRob/.github - ref: community-automations-updates + repository: learningequality/.github + ref: main token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4