diff --git a/.github/workflows/issue-trigger.yml b/.github/workflows/issue-trigger.yml index a27172bf16..2c18d4a746 100644 --- a/.github/workflows/issue-trigger.yml +++ b/.github/workflows/issue-trigger.yml @@ -4,19 +4,22 @@ on: types: [opened, transferred, assigned] jobs: - # Adds newly created issues onto project board in column 'New Issue Approval' + # Adds newly created issues onto project board in the default column 'New Issue Approval' + # unless overridden when issue has "LA website bot" in title, then 'Questions / In Review' Add-Issue-To-Project-Board: runs-on: ubuntu-latest if: ${{ github.event_name == 'issues' && github.event.action == 'opened' }} + env: + COLUMN_NAME: ${{ contains(github.event.issue.title, 'Hack for LA website bot') && 'Questions / In Review' || 'New Issue Approval' }} steps: - name: Add issue to project board - uses: alex-page/github-project-automation-plus@v0.8.3 id: add-issue-project-board + uses: alex-page/github-project-automation-plus@v0.8.3 with: project: Project Board - column: New Issue Approval + column: ${{ env.COLUMN_NAME }} repo-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} - + Add-Missing-Labels-To-Issues: runs-on: ubuntu-latest # Only trigger this action when an issue is newly created @@ -25,8 +28,8 @@ jobs: - uses: actions/checkout@v4 # Check if the issue has required labels - name: Check Labels - uses: actions/github-script@v7 id: check-labels + uses: actions/github-script@v7 with: script: | const script = require('./github-actions/trigger-issue/add-missing-labels-to-issues/check-labels.js') @@ -54,6 +57,7 @@ jobs: const script = require('./github-actions/trigger-issue/add-missing-labels-to-issues/post-labels-comment.js') script({g: github, c:context}, results) + #Asking for preliminary update when issue is assigned Ask-For-Preliminary-update: runs-on: ubuntu-latest @@ -72,7 +76,6 @@ jobs: const checklabels = script({g: github, c: context}) return checklabels - # Post the comment based on the result of the previous step - name: Post assigning issue comment id: assigned-comment @@ -81,4 +84,4 @@ jobs: script: | const results = ${{ steps.check-labels-prelim.outputs.result }} const script = require('./github-actions/trigger-issue/add-preliminary-comment/preliminary-update-comment.js') - script({g: github, c:context},results) \ No newline at end of file + script({g: github, c:context},results) diff --git a/.github/workflows/schedule-monthly.yml b/.github/workflows/schedule-monthly.yml index 59c69c399f..2a4647c8d5 100644 --- a/.github/workflows/schedule-monthly.yml +++ b/.github/workflows/schedule-monthly.yml @@ -1,12 +1,13 @@ name: Schedule Monthly -# This action runs at 11:00 UTC/ 3:00 PDT on the first day of the month. +# This action runs at 11:00 UTC/ 3:00 PDT on the first day of every month except January. on: schedule: - - cron: 0 11 1 * * + - cron: 0 11 1 2-12 * + workflow_dispatch: jobs: - trim_contributors: + Trim_Contributors: runs-on: ubuntu-latest if: github.repository == 'hackforla/website' @@ -15,7 +16,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} # Setup node - name: Setup node @@ -29,8 +30,61 @@ jobs: run: npm install working-directory: ./github-actions/trigger-schedule/github-data - # Run js file- check action logs for inactive members and removes from 'website-write' + # Run js file: checks contributor activity logs, removes two-month inactive members from + # 'website-write' team, then compiles list of one-month inactive members for notification - name: Trim Members env: token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} run: node github-actions/trigger-schedule/github-data/contributors-data.js + + # Upload artifact file to allow list sharing with next job "Create_New_Issue" + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: trim_job_artifact + path: inactive-Members.json + + + Create_New_Issue: + needs: Trim_Contributors + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Download artifact file from "Trim_Contributors" + - name: Download artifact + id: download-artifact + uses: actions/download-artifact@v3 + with: + name: trim_job_artifact + + # Extract and save artifact in usable form for next steps + - name: Extract artifact + id: extract-artifact + run: | + jq -c . inactive-Members.json > out-inactive-Members.json + echo "TRIM_LISTS=$(cat out-inactive-Members.json)" >> $GITHUB_ENV + + # Creates a new issue in 'hackforla/website' repo with the saved lists + - name: Create new issue + uses: actions/github-script@v7 + id: create-new-issue + 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; + + # 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/workflows/wr-schedule-monthly.yml b/.github/workflows/wr-schedule-monthly.yml new file mode 100644 index 0000000000..5192fc8b59 --- /dev/null +++ b/.github/workflows/wr-schedule-monthly.yml @@ -0,0 +1,46 @@ +name: WR Schedule Monthly + +on: + workflow_run: + workflows: ['Schedule Monthly'] + types: [completed] + +jobs: + On-success: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - run: echo "The 'Schedule Monthly' workflow succeeded. Continuing." + + # If 'Schedule Monthly' was succcesful, retrieve the owner URL + - name: Get owner url + id: get-owner-url + uses: actions/github-script@v7 + with: + script: | + const ownerURL = context.payload.repository.html_url; + console.log("Owner url: " + ownerURL); + return ownerURL; + + # Then retrieve the latest issue created in the repo (i.e. by 'Schedule Monthly') + - name: Get issue number + id: get-issue-number + uses: actions/github-script@v7 + with: + script: | + const newIssue = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state:"all", + per_page: 1, + page: 1, + }); + const newIssueNumber = newIssue['data'][0]['number']; + console.log("Latest issue number: " + newIssueNumber); + return newIssueNumber; + + # Automatically close this last issue- that is, the issue just created by 'Schedule Monthly' + - name: Auto close issue + run: gh issue close "${{ steps.get-owner-url.outputs.result }}/issues/${{ steps.get-issue-number.outputs.result }}" + env: + GH_TOKEN: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} diff --git a/github-actions/trigger-issue/add-missing-labels-to-issues/check-labels.js b/github-actions/trigger-issue/add-missing-labels-to-issues/check-labels.js index 390053cb9b..4e2850e535 100644 --- a/github-actions/trigger-issue/add-missing-labels-to-issues/check-labels.js +++ b/github-actions/trigger-issue/add-missing-labels-to-issues/check-labels.js @@ -3,6 +3,9 @@ const REQUIRED_LABELS = ['Complexity', 'role', 'Feature'] const LABEL_MISSING = ['Complexity: Missing', 'role missing', 'Feature Missing'] const COMPLEXITY_EXCEPTIONS = ['good first issue'] +// SPECIAL_CASE is for issue created by reference with issue title "Hack for LA website bot" (from "Review Inactive Team Members") +const SPECIAL_CASE = ['ready for dev lead','Feature: Administrative','size: 0.25pt','Complexity: Small','role: dev leads'] + // Global variables var github var context @@ -17,10 +20,16 @@ async function main({ g, c }) { github = g context = c const issueNum = context.payload.issue.number + const issueTitle = context.payload.issue.title const labels = obtainLabels() const filteredLabels = filterLabels(labels) - const labelsToAdd = checkLabels(filteredLabels) + let labelsToAdd = checkLabels(filteredLabels) + + // for SPECIAL_CASE noted above + if (issueTitle.includes('Hack for LA website bot')) { + labelsToAdd = SPECIAL_CASE; + } console.log('Labels to add: ', labelsToAdd) const result = await addLabels(labelsToAdd, filteredLabels) diff --git a/github-actions/trigger-schedule/github-data/contributors-data.js b/github-actions/trigger-schedule/github-data/contributors-data.js index e439130e0a..dd524d18bc 100644 --- a/github-actions/trigger-schedule/github-data/contributors-data.js +++ b/github-actions/trigger-schedule/github-data/contributors-data.js @@ -1,25 +1,31 @@ +const fs = require("fs"); const { Octokit } = require("@octokit/rest"); -const trueContributorsMixin = require("true-github-contributors"); // Extend Octokit with new contributor endpoints and construct instance of class with Auth token -Object.assign(Octokit.prototype, trueContributorsMixin); +Object.assign(Octokit.prototype); const octokit = new Octokit({ auth: process.env.token }); // Set variables to avoid hard-coding const org = 'hackforla'; const repo = 'website'; const team = 'website-write'; +const baseTeam = 'website'; -// Set date limits: at one month, warn contributor that they are -// inactive, and at two months remove contributor from team(s) -let oneMonthAgo = new Date(); // oneMonthAgo instantiated with date of "today" -oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); // then set oneMonthAgo from "today" +// 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 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 = oneMonthAgo.toISOString(); -let twoMonthsAgo = new Date(); // twoMonthsAgo instantiated with date of "today" -twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2); // then set twoMonthsAgo from "today" +let twoMonthsAgo = new Date(); // twoMonthsAgo instantiated with date of "today" +twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - twoMonths); // then set twoMonthsAgo from "today" twoMonthsAgo = twoMonthsAgo.toISOString(); + /** * Main function, immediately invoked */ @@ -33,7 +39,7 @@ twoMonthsAgo = twoMonthsAgo.toISOString(); console.log('-------------------------------------------------------'); console.log('Current members of ' + team + ':') console.log(currentTeamMembers) - + const removedContributors = await removeInactiveMembers(currentTeamMembers, contributorsTwoMonthsAgo); console.log('-------------------------------------------------------'); console.log('Removed members from ' + team + ' inactive since ' + twoMonthsAgo.slice(0, 10) + ':'); @@ -45,21 +51,23 @@ twoMonthsAgo = twoMonthsAgo.toISOString(); console.log('Notified members from ' + team + ' inactive since ' + oneMonthAgo.slice(0, 10) + ':'); console.log(notifiedContributors); + writeData(removedContributors, notifiedContributors); })(); /** * Function to fetch list of contributors with comments/commits/issues since date - * @return {Object} [List of active contributors] + * @returns {Object} allContributorsSinceOneMonthAgo - List of active contributors since one month ago + * @returns {Object} allContributorsSinceTwoMonthsAgo - List of active contributors since two months ago */ async function fetchContributors(){ let allContributorsSinceOneMonthAgo = {}; let allContributorsSinceTwoMonthsAgo = {}; - - // Fetch all contributors with commit, comment, and issue contributions + + // 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] + const dates = [oneMonthAgo, twoMonthsAgo]; for (const date of dates){ const allContributorsSince = {}; @@ -67,10 +75,9 @@ async function fetchContributors(){ let pageNum = 1; let result = []; - // Since Github only allows to fetch 100 items per request, we need to 'flip' pages + // Since Github only allows to fetch max 100 items per request, we need to 'flip' pages while(true){ - // Fetch 100 items from page number (`pageNum`) - // `oneMonthAgo` is a variable defined on top of the file + // Fetch 100 items per each page (`pageNum`) const contributors = await octokit.request(api, { owner: org, repo: repo, @@ -79,7 +86,7 @@ async function fetchContributors(){ page: pageNum }) - // If the API call returns an empty array, break out of loop- there is no additional data on that page. + // 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`) if(!contributors.data.length){ break; @@ -89,35 +96,31 @@ async function fetchContributors(){ } } - // Once we have looked at all pages and collected all the data, we create key-value pairs - // of recent contributors and store them in `allContributorsSince` object - - // The data that comes back from APIs is stored differently, i.e. `author.login` - // vs `user.login`, all we want is to extract the username of a contributor + // Once we have looked at all pages and collected all the data, we create key-value pairs of recent contributors and store + // them in the `allContributorsSince` object. The contributor data that comes back from each API are stored differently, + // i.e. `author.login` vs `user.login` vs `assignee.login`. We want to extract the contributors' usernames for each situation. for(const contributorInfo of result){ - // check if username is stored in author.login + // Check if username is stored in `author.login` if(contributorInfo.author){ allContributorsSince[contributorInfo.author.login] = true; - } else if(contributorInfo.user){ + } + // Check for username in `user.login`, but skip `user.login` for 3rd API + else if(contributorInfo.user && api != 'GET /repos/{owner}/{repo}/issues'){ allContributorsSince[contributorInfo.user.login] = true; - - // This check is done for "issues" API (3rd element in the APIs array). Sometimes a user who created - // an issue is not the same as the user assigned to that issue- we want to make sure that we count - // all assignees as active contributors as well. - if(contributorInfo.assignees && contributorInfo.assignees.length){ - contributorInfo.assignees.forEach(user => allContributorsSince[user.login] = true); - } - } else { - console.log('You should not be seeing this message...'); - } // END if...else - } // END for(const contributorInfo of result) - } // END for(const api of APIs) + } + // 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; + } + } + } if(date == oneMonthAgo){ allContributorsSinceOneMonthAgo = allContributorsSince; } else { allContributorsSinceTwoMonthsAgo = allContributorsSince; } - } // END for(date of dates) + } return [allContributorsSinceOneMonthAgo, allContributorsSinceTwoMonthsAgo]; } @@ -125,24 +128,21 @@ async function fetchContributors(){ /** * Function to return list of current team members - * @return {Array} [Current team members] + * @returns {Array} allMembers - Current team members */ async function fetchTeamMembers(){ let pageNum = 1; let teamResults = []; - + + // Fetch all members of team. Note: if total members exceed 100, we need to 'flip' pages while(true){ - // Fetch all members of team. Note: if total members exceed 100, we need to 'flip' pages const teamMembers = await octokit.request('GET /orgs/{org}/teams/{team_slug}/members', { org: org, team_slug: team, per_page: 100, page: pageNum }) - - // If the API call returns an empty array, break out of loop- there is no additional data on that page. - // Else if data is returned, push it to `result` and increase the page number (`pageNum`) if(!teamMembers.data.length){ break; } else { @@ -161,9 +161,9 @@ async function fetchTeamMembers(){ /** * Function to return list of contributors that have been inactive since twoMonthsAgo - * @param {Object} allMembers [List of active team] - * @param {Object} recentContributors [List of active contributors] - * @return {Array} [removed members] + * @param {Object} currentTeamMembers - List of active team members + * @param {Object} recentContributors - List of active contributors + * @returns {Array} removed members - List of members that were removed */ async function removeInactiveMembers(currentTeamMembers, recentContributors){ const removedMembers = []; @@ -171,6 +171,21 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){ // 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 + const baseMember = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', { + org: org, + team_slug: baseTeam, + username: username, + }) + // If response status is not 200, need to add member to 'base' team before deleting + if(baseMember.status != 200){ + await octokit.request('PUT /orgs/{org}/teams/{team_slug}/memberships/{username}', { + org: org, + team_slug: baseTeam, + username: userName, + role: 'member', + }) + } // 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}', { @@ -189,37 +204,39 @@ async function removeInactiveMembers(currentTeamMembers, recentContributors){ /** * Function to check if a member is set for removal - * @param {String} member [member's username] - * @return {Boolean} [true/false] + * @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; + // 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 'website' repo (within the last 30 days), they are - // not consider for removal as they are new; + // 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' or member is not be removed; + // 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 + // Get user's membership status const userMembership = await octokit.request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', { org: org, team_slug: team, username: member, }) - // if a user is the team's maintainer, return 'false'. We do not remove maintainers; - if(userMembership.data.role === 'maintainer') return false; - - // else this user is an inactive member of the team thus remove; + // If a user is a team 'maintainer', log their name and return 'false'. We do not remove maintainers + if(userMembership.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 return true; } @@ -227,9 +244,9 @@ async function toRemove(member){ /** * Function to return list of contributors that have been inactive since oneMonthAgo - * @param {Object} teamMembers [List of team members] - * @param {Object} recentContributors [List of active contributors] - * @return {Array} [removed members] + * @param {Array} updatedTeamMembers - List of updated team members + * @param {Array} recentContributors - List of recent contributors + * @returns {Array} - List of members to be notified (that they are on the list to be removed) */ async function notifyInactiveMembers(updatedTeamMembers, recentContributors){ const notifiedMembers = []; @@ -245,3 +262,30 @@ async function notifyInactiveMembers(updatedTeamMembers, recentContributors){ } return notifiedMembers; } + + + +/** + * Function to save inactive members list to local for use in next job + * @param {Array} removedContributors - List of removed contributors + * @param {Array} notifiedContributors - List of contributors to be notified + * @returns {void} + */ +function writeData(removedContributors, notifiedContributors){ + + // Combine removed and notified contributor lists into one dict + let inactiveMemberLists = {}; + inactiveMemberLists["removedContributors"] = removedContributors; + inactiveMemberLists["notifiedContributors"] = notifiedContributors; + + fs.writeFile('inactive-Members.json', JSON.stringify(inactiveMemberLists, null, 2), (err) => { + if (err) throw err; + console.log('-------------------------------------------------------'); + 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 index 71543569cc..b029968323 100644 --- a/github-actions/trigger-schedule/list-inactive-members/comment-issue.js +++ b/github-actions/trigger-schedule/list-inactive-members/comment-issue.js @@ -8,10 +8,12 @@ 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"; @@ -19,7 +21,7 @@ const commentOnIssue = async (agendaAndNotesIssueNumber, newIssueNumber) => { owner, repo, issue_number: agendaAndNotesIssueNumber, - body: `**Review Inactive Members:** #${newIssueNumber}`, + body: `**Review Inactive Team Members:** #${newIssueNumber}`, }); }; 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 6299d181a6..66f363550f 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 @@ -4,54 +4,117 @@ var github; var context; -async function main({ g, c }, list) { +async function main({ g, c }, artifactContent) { github = g; context = c; + // Retrieve lists data from json file written in previous step + let inactiveLists = JSON.parse(artifactContent); + const owner = "hackforla"; const repo = "website"; - // 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, list); + // 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); const issueId = issue.id; const issueNumber = issue.number; - // get project id, in order to get the column id of `New Issue Approval` in `Project Board` + // Get project id, in order to get the column id of `New Issue Approval` in `Project Board` const projectId = await getProjectId(owner, repo); - // get column id, in order to create a project card in `Project Board` and place in `New Issue Approval` + // Get column id, in order to create a project card in `Project Board` and place in `New Issue Approval` const columnId = await getColumnId(projectId); - // create the project card, which links to the issue created on line 16 + // Create the project card, which links to the issue created in createIssue() above await createProjectCard(issueId, columnId); - // return issue number: going to be using this number to link the issue when commenting on the `Dev/PM Agenda and Notes` + // Return issue number used to reference the issue when commenting on the `Dev/PM Agenda and Notes` return issueNumber; } -const createIssue = async (owner, repo, list) => { - let listWithNewLine = list.join("\n"); - let body = "**Inactive Members:**\n\n" + listWithNewLine; +const createIssue = async (owner, repo, inactiveLists) => { + // Splits inactivesList into lists of removed contributors and of those to be notified + let removeList = inactiveLists['removedContributors']; + let notifyList = inactiveLists['notifiedContributors']; + + let removedList = removeList.map(x => "@" + x).join("\n"); + let notifiedList = notifyList.map(x => "@" + x).join("\n"); + + // This finds all issues in the repo and returns the only the number for the last issue created. + // Add 1 to this issue number to get the number for the next issue- i.e. the one being created. + let thisIssuePredict = await github.rest.issues.listForRepo({ + owner, + repo, + state:"all", + per_page: 1, + page: 1, + }); + 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 = [ - "Ready for dev lead", - "Ready for product", + "Feature: Administrative", + "Feature: Onboarding/Contributing.md", + "role: dev leads", "Complexity: Small", "Size: 0.5pt", ]; - const title = "Review Inactive Members"; + // Note that 8 represents ".08 Team workflow" i.e. the 8th workflow on HfLAs Project Board + let milestone = 8; const issue = await github.rest.issues.create({ owner, repo, title, body, labels, + milestone, }); return issue.data; }; const getProjectId = async (owner, repo) => { - // get all projects for the repo + // Get all projects for the repo let projects = await github.rest.projects.listForRepo({ owner, repo, }); - // select project with name `Project Board`, access the `id` + // Select project with name `Project Board` then access the project `id` let projectId = projects.data.filter((project) => { return (project.name = "Project Board"); })[0].id; @@ -59,11 +122,11 @@ const getProjectId = async (owner, repo) => { }; const getColumnId = async (projectId) => { - // get all columns in the project board + // Get all columns in the project board let columns = await github.rest.projects.listColumns({ project_id: projectId, }); - // select column with name `New Issue Approval`, access the `id + // Select column with name `New Issue Approval` then access the column `id` let columnId = columns.data.filter((column) => { return column.name === "New Issue Approval"; })[0].id; diff --git a/github-actions/trigger-schedule/list-inactive-members/get-list.js b/github-actions/trigger-schedule/list-inactive-members/get-list.js deleted file mode 100644 index a2a6644e02..0000000000 --- a/github-actions/trigger-schedule/list-inactive-members/get-list.js +++ /dev/null @@ -1,69 +0,0 @@ -// Import modules - -// Global variables -var github; -var context; - -async function main({ g, c }) { - github = g; - context = c; - - const org = "hackforla"; - const teamSlug = "website-write"; - const owner = "hackforla"; - const repo = "website"; - - // get number of team members for the website-write team (need this number to determine amount of page numbers to fetch from Github API) - // Github API limits 100 max results per request - let pageNumbers = await getNumberOfPages(org, teamSlug, github); - let allTeamMembers = await getAllMembers(org, teamSlug, pageNumbers); - - return await selectMembersWithNoIssues(allTeamMembers, owner, repo); -} - -const getNumberOfPages = async (org, teamSlug) => { - // get number of pages, needed for `getMembersWithoutIssues` function. GithubAPI has a return limit of 100 results => over 300 team members in website-write - let websiteWriteTeam = await github.rest.teams.getByName({ - org, - team_slug: teamSlug, - }); - - let membersCount = websiteWriteTeam.data.members_count; - let pageNumbers = Math.ceil(membersCount / 100); - return pageNumbers; -}; - -const getAllMembers = async (org, teamSlug, pageNumbers) => { - // get all team members - let allTeamMembers = []; - for (let currPage = 1; currPage <= pageNumbers; currPage += 1) { - let teamMembers = await github.rest.teams.listMembersInOrg({ - org, - team_slug: teamSlug, - per_page: 100, - page: currPage, - }); - allTeamMembers = allTeamMembers.concat(teamMembers.data); - } - return allTeamMembers; -}; - -const selectMembersWithNoIssues = async (allTeamMembers, owner, repo) => { - // select team members without open issues - let inactiveMembers = []; - for (let member of allTeamMembers) { - let assignee = member.login; - let memberIssues = await github.rest.issues.listForRepo({ - owner, - repo, - state: "open", - assignee, - }); - if (memberIssues.data.length === 0) { - inactiveMembers.push(assignee); - } - } - return inactiveMembers; -}; - -module.exports = main;