diff --git a/.github/workflows/schedule-monthly.yml b/.github/workflows/schedule-monthly.yml index 6995cd3811..a20df497f1 100644 --- a/.github/workflows/schedule-monthly.yml +++ b/.github/workflows/schedule-monthly.yml @@ -72,19 +72,7 @@ jobs: with: github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} script: | - const artifactContent = process.env.TRIM_LISTS; - const script = require('./github-actions/trigger-schedule/list-inactive-members/create-new-issue.js'); - const createNewIssue = script({g: github, c: context}, artifactContent); - return createNewIssue; + const artifactContent = process.env.TRIM_LISTS + const script = require('./github-actions/trigger-schedule/list-inactive-members/create-new-issue.js') + script({g: github, c: context}, artifactContent) - # Comments on issue #2607, notifying leads that the above issue has been created - - name: Comment issue - uses: actions/github-script@v7 - id: comment-issue - with: - github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} - script: | - const script = require('./github-actions/trigger-schedule/list-inactive-members/comment-issue.js'); - const newIssueNumber = ${{ steps.create-new-issue.outputs.result }}; - script({g: github, c: context}, newIssueNumber); - diff --git a/github-actions/trigger-schedule/github-data/contributors-data.js b/github-actions/trigger-schedule/github-data/contributors-data.js index dd524d18bc..a2320c45f5 100644 --- a/github-actions/trigger-schedule/github-data/contributors-data.js +++ b/github-actions/trigger-schedule/github-data/contributors-data.js @@ -10,15 +10,17 @@ const org = 'hackforla'; const repo = 'website'; const team = 'website-write'; const baseTeam = 'website'; +const maintTeam = 'website-maintain'; // Set date limits: we are sorting inactive members into groups to warn after 1 month and remove after 2 months. // Since the website team takes off the month of December, the January 1st run is skipped (via `schedule-monthly.yml`). // The February 1st run keeps the 1 month inactive warning, but changes removal to 3 months inactive (skipping December). let today = new Date(); +let oneMonth = (today.getMonth() == 1) ? 2 : 1; // If month is "February" == 1, then oneMonth = 2 months ago let twoMonths = (today.getMonth() == 1) ? 3 : 2; // If month is "February" == 1, then twoMonths = 3 months ago let oneMonthAgo = new Date(); // oneMonthAgo instantiated with date of "today" -oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); // then set oneMonthAgo from "today" +oneMonthAgo.setMonth(oneMonthAgo.getMonth() - oneMonth); // then set oneMonthAgo from "today" oneMonthAgo = oneMonthAgo.toISOString(); let twoMonthsAgo = new Date(); // twoMonthsAgo instantiated with date of "today" twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - twoMonths); // then set twoMonthsAgo from "today" @@ -30,22 +32,26 @@ twoMonthsAgo = twoMonthsAgo.toISOString(); * Main function, immediately invoked */ (async function main(){ - const [contributorsOneMonthAgo, contributorsTwoMonthsAgo] = await fetchContributors(); + const [contributorsOneMonthAgo, contributorsTwoMonthsAgo, inactiveWithOpenIssue] = await fetchContributors(); console.log('-------------------------------------------------------'); - console.log('List of active contributors since' + ' ⏰ ' + oneMonthAgo.slice(0, 10) + ':'); + console.log('List of active contributors since ' + oneMonthAgo.slice(0, 10) + ':'); console.log(contributorsOneMonthAgo); - const currentTeamMembers = await fetchTeamMembers(); + const currentTeamMembers = await fetchTeamMembers(team); console.log('-------------------------------------------------------'); console.log('Current members of ' + team + ':') console.log(currentTeamMembers) - const removedContributors = await removeInactiveMembers(currentTeamMembers, contributorsTwoMonthsAgo); + const [removedContributors, cannotRemoveYet] = await removeInactiveMembers(currentTeamMembers, contributorsTwoMonthsAgo, inactiveWithOpenIssue); console.log('-------------------------------------------------------'); console.log('Removed members from ' + team + ' inactive since ' + twoMonthsAgo.slice(0, 10) + ':'); console.log(removedContributors); - const updatedTeamMembers = await fetchTeamMembers(); + console.log('-------------------------------------------------------'); + console.log('Members inactive since ' + twoMonthsAgo.slice(0, 10) + ' with open issues preventing removal:'); + console.log(cannotRemoveYet); + + const updatedTeamMembers = await fetchTeamMembers(team); const notifiedContributors = await notifyInactiveMembers(updatedTeamMembers, contributorsOneMonthAgo); console.log('-------------------------------------------------------'); console.log('Notified members from ' + team + ' inactive since ' + oneMonthAgo.slice(0, 10) + ':'); @@ -64,7 +70,11 @@ twoMonthsAgo = twoMonthsAgo.toISOString(); async function fetchContributors(){ let allContributorsSinceOneMonthAgo = {}; let allContributorsSinceTwoMonthsAgo = {}; + let inactiveWithOpenIssue = {}; + // Members on 'website-maintain' team considered permanent members + const permanentMembers = await fetchTeamMembers(maintTeam); + // Fetch all contributors with commit, comment, and issue (assignee) contributions const APIs = ['GET /repos/{owner}/{repo}/commits', 'GET /repos/{owner}/{repo}/issues/comments', 'GET /repos/{owner}/{repo}/issues']; const dates = [oneMonthAgo, twoMonthsAgo]; @@ -84,7 +94,7 @@ async function fetchContributors(){ since: date, per_page: 100, page: pageNum - }) + }); // If the API call returns an empty array, break out of loop- there is no additional data. // Else if data is returned, push it to `result` and increase the page number (`pageNum`) @@ -104,33 +114,107 @@ async function fetchContributors(){ if(contributorInfo.author){ allContributorsSince[contributorInfo.author.login] = true; } - // Check for username in `user.login`, but skip `user.login` for 3rd API + // Check for username in `user.login`, but skip `user.login` covered by 3rd API else if(contributorInfo.user && api != 'GET /repos/{owner}/{repo}/issues'){ allContributorsSince[contributorInfo.user.login] = true; } // This check is done for `/issues` (3rd) API. Sometimes a user who created an issue is not the same as the // assignee on that issue- we want to make sure that we count all assignees as active contributors as well. - else if(contributorInfo.assignee){ - allContributorsSince[contributorInfo.assignee.login] = true; - } - } - } + // We only want to run this check if the assignee is not counted as an active contributor yet. + else if((contributorInfo.assignee) && (contributorInfo.assignee.login in allContributorsSince === false)){ + const issueNum = contributorInfo.number; + const timeline = await getEventTimeline(issueNum); + const assignee = contributorInfo.assignee.login; + const responseObject = await isEventOutdated(date, timeline, assignee); + // If timeline is not outdated, add member to `allContributorsSince` + if(responseObject.result === false){ + allContributorsSince[assignee] = true; + } + // If timeline is more than two months ago, and the issue title does not include + // the words "Pre-work Checklist", add to open issues with inactive comments + else { + if(date == twoMonthsAgo && !contributorInfo.title.includes("Pre-work Checklist")){ + inactiveWithOpenIssue[assignee] = issueNum; + } + } + } + } + } + // Add permanent members from 'website-maintain' to list of active contributors + for(const permanentMember in permanentMembers){ + allContributorsSince[permanentMember] = true; + } if(date == oneMonthAgo){ allContributorsSinceOneMonthAgo = allContributorsSince; } else { allContributorsSinceTwoMonthsAgo = allContributorsSince; } } - return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo]; + return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo, inactiveWithOpenIssue]; +} + + +/* + * Helper functions for fetchContributors() + * + * + */ +async function getEventTimeline(issueNum) { + let timelineArray = [] + let page = 1 + while (true) { + try { + const results = await octokit.rest.issues.listEventsForTimeline({ + owner: org, + repo: repo, + issue_number: issueNum, + per_page: 100, + page: page, + }); + if (results.data.length) { + timelineArray = timelineArray.concat(results.data); + } else { + break + } + } catch (err) { + console.log(err); + continue + } + finally { + page++ + } + } + return timelineArray +} + + +function isEventOutdated(date, timeline, assignee) { + let lastAssignedTimestamp = null; + for (let i = timeline.length - 1; i >= 0; i--) { + let eventObj = timeline[i]; + let eventType = eventObj.event; + let eventTimestamp = eventObj.updated_at || eventObj.created_at; + + // update the lastAssignedTimestamp if this is the last (most recent) time an assignee was assigned to the issue + if (!lastAssignedTimestamp && eventType === 'assigned' && assignee === (eventObj.assignee.login)) { + lastAssignedTimestamp = eventTimestamp; + } + } + // If the assignee was assigned later than the 'date', the issue is not outdated so return false + if (lastAssignedTimestamp && (lastAssignedTimestamp >= date)) { + return { result: false }; + } + return { result: true }; } /** * Function to return list of current team members + * @param {String} team_slug - default to 'website-write' team * @returns {Array} allMembers - Current team members */ -async function fetchTeamMembers(){ +async function fetchTeamMembers(fetchTeam){ let pageNum = 1; let teamResults = []; @@ -139,7 +223,7 @@ async function fetchTeamMembers(){ while(true){ const teamMembers = await octokit.request('GET /orgs/{org}/teams/{team_slug}/members', { org: org, - team_slug: team, + team_slug: fetchTeam, per_page: 100, page: pageNum }) @@ -165,13 +249,14 @@ async function fetchTeamMembers(){ * @param {Object} recentContributors - List of active contributors * @returns {Array} removed members - List of members that were removed */ -async function removeInactiveMembers(currentTeamMembers, recentContributors){ +async function removeInactiveMembers(currentTeamMembers, recentContributors, inactiveWithOpenIssue){ const removedMembers = []; + const cannotRemoveYet = {}; // Loop over team members and remove them from the team if they are not in recentContributors for(const username in currentTeamMembers){ if (!recentContributors[username]){ - // Prior to deletion, confirm that member is on the 'base', i.e. 'website', team + // Prior to deletion, confirm that member is on the 'base' === 'website' team const baseMember = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', { org: org, team_slug: baseTeam, @@ -182,22 +267,28 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){ await octokit.request('PUT /orgs/{org}/teams/{team_slug}/memberships/{username}', { org: org, team_slug: baseTeam, - username: userName, + username: username, role: 'member', }) + console.log('Member added to \'website\' team: ' + username); + } + // Remove member from the team if they don't pass additional checks in `shouldRemoveOrNotify` function + if(await shouldRemoveOrNotify(username)){ + // But if member has an open issue, don't remove + if(username in inactiveWithOpenIssue){ + cannotRemoveYet[username] = inactiveWithOpenIssue[username]; + } else { + await octokit.request('DELETE /orgs/{org}/teams/{team_slug}/memberships/{username}', { + org: org, + team_slug: team, + username: username, + }) + removedMembers.push(username); + } } - // Remove contributor from a team if they don't pass additional checks in `toRemove` function - if(await toRemove(username)){ - await octokit.request('DELETE /orgs/{org}/teams/{team_slug}/memberships/{username}', { - org: org, - team_slug: team, - username: username, - }) - removedMembers.push(username); - } } } - return removedMembers; + return [removedMembers, cannotRemoveYet]; } @@ -207,36 +298,32 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){ * @param {String} member - Member's username * @returns {Boolean} - true/false */ -async function toRemove(member){ - // Collect user's repos and see if they recently joined hackforla/website; - // Note: user might have > 100 repos, the code below will need adjustment (see 'flip' pages); - const repos = await octokit.request('GET /users/{username}/repos', { - username: member, - per_page: 100 - }) - - // If a user recently* cloned the 'website' repo (*within the last 30 days), then - // they are new members and are not considered for notification or removal. - for(const repository of repos.data){ - // If repo is recently cloned, return 'false' so that member is not removed - if(repository.name === repo && repository.created_at > oneMonthAgo){ - return false; - } - } - - // Get user's membership status - const userMembership = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', { +async function shouldRemoveOrNotify(member){ + + // Get member's membership status: if member is a team 'Maintainer', return false- we don't remove maintainers + const membershipStatus = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', { org: org, team_slug: team, username: member, }) - - // If a user is a team 'maintainer', log their name and return 'false'. We do not remove maintainers - if(userMembership.data.role === 'maintainer'){ + if(membershipStatus.data.role === 'maintainer'){ console.log("This inactive member is a 'Maintainer': " + member); return false; } - // Else this user is an inactive member of the team and should be notified or removed + + // Run check to see if member cloned the 'website' repo within the last 30 days. If so do not notify + // because they are new members. (This will not catch new members who did not name their repos 'website'.) + try { + const repoData = await octokit.request('GET /repos/{username}/{repo}', { + username: member, + repo: repo, + }); + if(repoData.created_at > oneMonthAgo){ + return false; + } + } catch {} + + // Else this member is inactive and should be notified or removed from team return true; } @@ -254,8 +341,8 @@ async function notifyInactiveMembers(updatedTeamMembers, recentContributors){ // Loop over team members and add to "notify" list if they are not in recentContributors for(const username in updatedTeamMembers){ if (!recentContributors[username]){ - // Remove contributor from a team if they don't pass additional checks in `toRemove` function - if(await toRemove(username)){ + // Check whether member should be added to notifiedMembers list + if(await shouldRemoveOrNotify(username)){ notifiedMembers.push(username) } } @@ -284,8 +371,4 @@ function writeData(removedContributors, notifiedContributors){ console.log("File 'inactive-Members.json' saved successfully!"); }); - fs.readFile('inactive-Members.json', (err, data) => { - if (err) throw err; - console.log("File 'inactive-Members.json' read successfully!"); - }); - } +} diff --git a/github-actions/trigger-schedule/list-inactive-members/comment-issue.js b/github-actions/trigger-schedule/list-inactive-members/comment-issue.js deleted file mode 100644 index b029968323..0000000000 --- a/github-actions/trigger-schedule/list-inactive-members/comment-issue.js +++ /dev/null @@ -1,28 +0,0 @@ -// Import modules - -// Global variables -var github; -var context; - -async function main({ g, c }, newIssueNumber) { - github = g; - context = c; - - // Issue #2607 is the `Dev/PM Agenda and Notes` - let agendaAndNotesIssueNumber = 2607; - await commentOnIssue(agendaAndNotesIssueNumber, newIssueNumber); -} - -// Add a link to the `Review Inactive Team Members` issue -const commentOnIssue = async (agendaAndNotesIssueNumber, newIssueNumber) => { - const owner = "hackforla"; - const repo = "website"; - await github.rest.issues.createComment({ - owner, - repo, - issue_number: agendaAndNotesIssueNumber, - body: `**Review Inactive Team Members:** #${newIssueNumber}`, - }); -}; - -module.exports = main; diff --git a/github-actions/trigger-schedule/list-inactive-members/create-new-issue.js b/github-actions/trigger-schedule/list-inactive-members/create-new-issue.js index 66f363550f..e4421eda52 100644 --- a/github-actions/trigger-schedule/list-inactive-members/create-new-issue.js +++ b/github-actions/trigger-schedule/list-inactive-members/create-new-issue.js @@ -1,4 +1,6 @@ // Import modules +const issueTemplateParser = require('../../utils/issue-template-parser'); +const postComment = require('../../utils/post-issue-comment'); // Global variables var github; @@ -11,8 +13,9 @@ async function main({ g, c }, artifactContent) { // Retrieve lists data from json file written in previous step let inactiveLists = JSON.parse(artifactContent); - const owner = "hackforla"; - const repo = "website"; + const owner = context.repo.owner; + const repo = context.repo.repo; + const agendaIssueNum = 2607; // Issue number of the Dev/PM meeting agenda on Mondays // Create a new issue in repo, return the issue id for later: creating the project card linked to this issue const issue = await createIssue(owner, repo, inactiveLists); @@ -24,12 +27,13 @@ async function main({ g, c }, artifactContent) { const columnId = await getColumnId(projectId); // Create the project card, which links to the issue created in createIssue() above await createProjectCard(issueId, columnId); - // Return issue number used to reference the issue when commenting on the `Dev/PM Agenda and Notes` - return issueNumber; + // Add issue number used to reference the issue and comment on the `Dev/PM Agenda and Notes` + const commentBody = `**Review Inactive Team Members:** #` + issueNumber; + await postComment(agendaIssueNum, commentBody, github, context); } const createIssue = async (owner, repo, inactiveLists) => { - // Splits inactivesList into lists of removed contributors and of those to be notified + // Splits inactiveLists into lists of removed contributors and of those to be notified let removeList = inactiveLists['removedContributors']; let notifyList = inactiveLists['notifiedContributors']; @@ -47,56 +51,21 @@ const createIssue = async (owner, repo, inactiveLists) => { }); let thisIssueNumber = thisIssuePredict['data'][0]['number'] + 1 - let title = "Review Inactive Team Members"; - let body = "# Review of Inactive Website Team Members\n" - + "## Inactive Members\n" - + "Developers: If your name is on the following list, our team bot has determined that you have not been active with the Website team in the last 30 days. If you remain inactive or we don't hear back from you in the upcoming weeks, we will unassign you from any issues you may be working on and remove you from the 'website-write' team.\n\n" - + notifiedList + "\n\n" - + "### How you can remain active\n" - + "The bot is checking for the following activity:\n" - + "- If you are assigned to an issue, that you have provided an update on the issue in the past 30 days. The updates are due weekly.\n" - + "- If your issue is a \`Draft\` in the \"New Issue Approval\" column, that you have added to it within the last 30 days.\n" - + "- If you are reviewing PRs, that you have posted a review comment within the past 30 days.\n\n" - + "If you have been inactive in the last 30 days (using the above measurements), you can become active again by doing at least one of the above actions. The bot will automatically remove you from next month's list.\n\n" - + "### Did we make a mistake?\n" - + "If you were active during the last 30 days (using the above measurements) and the bot made a mistake, let us know: Copy the following message into a comment below, add the pertinent issue or PR number, then select \"Comment\". Next, select \"...\", then \"Reference in a new issue\". [Watch demo](https://github.com/t-will-gillis/website/assets/40799239/2cf3c3d5-53db-4ad9-845d-645237a0dab4)\n\n" - + "```\n" - + "The Hack for LA website bot made a mistake!\n" - + "I am responding to Issue #" + thisIssueNumber + " because I have been active.\n" - + "See my Issue #{_list issue_} or my review in PR #{_list PR_} \n\n" - + "## Dev Leads\n" - + "This member believes that the bot has made a mistake by placing them on the \"Inactive Member\" list. Please see the referenced issue.\n\n" - + "```\n" - + "After you leave the comment, please send us a Slack message on the \"hfla-site\" channel with a link to your comment.\n\n" - + "### Temporary leave\n" - + "If you have taken a temporary leave, and you have been authorized to keep your assignment to an issue: \n" - + "- Your issue should be in the \"Questions/ In Review\" column, with the \`ready for dev lead\` label and a note letting us know when you will be back.\n" - + "- We generally encourage you to unassign yourself from the issue and allow us to return it to the \"Prioritized backlog\" column. However, exceptions are sometimes made.\n" - + "## Removed Members\n" - + "Our team bot has determined that the following member(s) have not been active with the Website team for over 60 days, and therefore the member(s) have been removed from the 'website-write' team.\n\n" - + removedList + "\n\n" - + "If this is a mistake or if you would like to return to the Hack for LA Website team, let us know: Copy the following message into a comment below then select \"Comment\". Next, select \"...\", then \"Reference in a new issue\". [Watch demo](https://github.com/t-will-gillis/website/assets/40799239/2cf3c3d5-53db-4ad9-845d-645237a0dab4)\n\n" - + "```\n" - + "The Hack for LA website bot removed me!\n" - + "I am responding to Issue #" + thisIssueNumber + " because I want to come back.\n" - + "Please add me back to the \'website-write\' team, I am ready to work on an issue now.\n\n" - + "## Dev Leads\n" - + "This member was previously on the \'website-write\' team and is now asking to be reactivated. Please see the referenced issue.\n\n" - + "```\n" - + "After you leave the comment, please send us a Slack message on the \"hfla-site\" channel with a link to your comment.\n\n" - + "## Dev Leads\n" - + "This issue is closed automatically after creation. If a link referred you to this issue, check whether: \n" - + "- A \"Removed Member\" is requesting reactivation to the 'website-write' team. If so, confirm that the member is/was part of HfLA, then reactivate and inform them via a comment on the referring issue as well as via Slack.\n" - + "- An \"Inactive Member\" believes that the bot has made a mistake. If so, determine whether the member has or has not been active based on the issue or PR number provided in the member's comment. If multiple members are being inappropriately flagged as \"inactive\" by the bot, then submit an ER / Issue in order to deactivate the `schedule-monthly.yml` workflow so that the cause of the bug can be investigated and resolved.\n\n" - let labels = [ - "Feature: Administrative", - "Feature: Onboarding/Contributing.md", - "role: dev leads", - "Complexity: Small", - "Size: 0.5pt", - ]; - // Note that 8 represents ".08 Team workflow" i.e. the 8th workflow on HfLAs Project Board - let milestone = 8; + // Uses issueTemplateParser to pull the relevant data from the issue template + const pathway = 'github-actions/trigger-schedule/list-inactive-members/inactive-members.md'; + const issueObject = issueTemplateParser(pathway); + + let title = issueObject['title']; + let labels = issueObject['labels']; + let milestone = parseInt(issueObject['milestone']); + let body = issueObject['body']; + + // Replace variables in issue template body + body = body.replace('${notifiedList}', notifiedList); + body = body.replace('${removedList}', removedList); + body = body.replaceAll('${thisIssueNumber}', thisIssueNumber); + + // Create issue const issue = await github.rest.issues.create({ owner, repo, diff --git a/github-actions/trigger-schedule/list-inactive-members/inactive-members.md b/github-actions/trigger-schedule/list-inactive-members/inactive-members.md new file mode 100644 index 0000000000..2dcafe8700 --- /dev/null +++ b/github-actions/trigger-schedule/list-inactive-members/inactive-members.md @@ -0,0 +1,66 @@ +--- +name: Review Inactive Team Members +about: Issue template used only by `schedule-monthly.yml` +title: "Review Inactive Team Members" +labels: ['Feature: Administrative', 'Feature: Onboarding/Contributing.md', 'role: dev leads', 'Complexity: Small', 'size: 0.5pt'] +milestone: 8 +assignees: '' +--- + + + +# Review of Inactive Website Team Members +## Inactive Members +Developers: If your name is on the following list, our team bot has determined that you have not been active with the Website team in the last 30 days. If you remain inactive or we don't hear back from you in the upcoming weeks, we will unassign you from any issues you may be working on and remove you from the 'website-write' team. + +${notifiedList} + +### How you can remain active +The bot is checking for the following activity: +- If you are assigned to an issue, that you have provided an update on the issue in the past 30 days. The updates are due weekly. +- If your issue is a `Draft` in the "New Issue Approval" column, that you have added to it within the last 30 days. +- If you are reviewing PRs, that you have posted a review comment within the past 30 days. + +If you have been inactive in the last 30 days (using the above measurements), you can become active again by doing at least one of the above actions. The bot will automatically remove you from next month's list. + +### Did we make a mistake? +If you were active during the last 30 days (using the above measurements) and the bot made a mistake, let us know: Copy the following message into a comment below, add the pertinent issue or PR number, then select "Comment". Next, select "...", then "Reference in a new issue". [Watch demo](https://github.com/t-will-gillis/website/assets/40799239/59d45792-6950-46f0-a310-7c1ecd0c87be) +``` +The Hack for LA website bot made a mistake! +I am responding to Issue #${thisIssueNumber} because I have been active. +See my Issue #{_list issue_} or my review in PR #{_list PR_} + +## Dev Leads +This member believes that the bot has made a mistake by placing them on the "Inactive Member" list. Please see the referenced issue. + +``` +After you leave the comment, please send us a Slack message on the "hfla-site" channel with a link to your comment. + +### Temporary leave +If you have taken a temporary leave, and you have been authorized to keep your assignment to an issue: +- Your issue should be in the "Questions/ In Review" column, with the `ready for dev lead` label and a note letting us know when you will be back. +- We generally encourage you to unassign yourself from the issue and allow us to return it to the "Prioritized backlog" column. However, exceptions are sometimes made. + +## Removed Members +Developers: If your name is on the following list, our team bot has determined that you have not been active with the Website team for over 60 days, and therefore you have been removed from the 'website-write' team. + +${removedList} + +If this is a mistake or if you would like to return to the Hack for LA Website team, let us know: Copy the following message into a comment below then select "Comment". Next, select "...", then "Reference in a new issue". [Watch demo](https://github.com/t-will-gillis/website/assets/40799239/59d45792-6950-46f0-a310-7c1ecd0c87be) +``` +The Hack for LA website bot removed me! +I am responding to Issue #${thisIssueNumber} because I want to come back. +Please add me back to the 'website-write' team, I am ready to work on an issue now. + +## Dev Leads +This member was previously on the 'website-write' team and is now asking to be reactivated. Please see the referenced issue. + +``` +After you leave the comment, please send us a Slack message on the "hfla-site" channel with a link to your comment. + +## Dev Leads +This issue is closed automatically after creation. If a link referred you to this issue, check whether: +- A "Removed Member" is requesting reactivation to the 'website-write' team. If so, confirm that the member is/was a part of HfLA, then reactivate and inform them via a comment on the referring issue as well as via Slack. +- An "Inactive Member" believes that the bot has made a mistake. If so, determine whether the member has or has not been active based on the issue or PR number provided in the member's comment. If multiple members are being inappropriately flagged as "inactive" by the bot, then submit an ER / Issue in order to deactivate the `schedule-monthly.yml` workflow so that the cause of the bug can be investigated and resolved. diff --git a/github-actions/utils/issue-template-parser.js b/github-actions/utils/issue-template-parser.js new file mode 100644 index 0000000000..6f8a398c9e --- /dev/null +++ b/github-actions/utils/issue-template-parser.js @@ -0,0 +1,37 @@ + // import modules +const fs = require('fs'); + +/** + * Parses a GitHub markdwon/ schema issue template + * + * @param {string} filepath - filepath to issue template + * @return {array} issueObject - parameters to create issue + */ + +function issueTemplateParser(filepath) { + + // Read contents of issue template + const templateData = fs.readFileSync(filepath, 'utf8'); + const data = templateData.split('---'); + + // Parse front matter i.e. between '---' delimiters, create object + const frontMatter = data[1].trim().split('\n'); + const issueObject = frontMatter.reduce((acc, item) => { + const colonIndex = item.indexOf(':'); + const key = item.slice(0, colonIndex).trim(); + const value = item.slice(colonIndex + 1).trim(); + if (value.startsWith('[') && value.endsWith(']')) { + acc[key] = value.slice(1, -1).replaceAll(/["']/g,'').split(',').map(str => str.trim()); + } else { + acc[key] = value.replaceAll(/["']/g,''); + } + return acc; + }, {}); + + // Add body entry to issueObject + issueObject['body'] = data[2].trim(); + + return issueObject +} + +module.exports = issueTemplateParser;