diff --git a/.github/workflows/monitor-inactive-issues.yml b/.github/workflows/monitor-inactive-issues.yml new file mode 100644 index 00000000000..d6964b67dd6 --- /dev/null +++ b/.github/workflows/monitor-inactive-issues.yml @@ -0,0 +1,48 @@ +name: Monitor Inactive Open Issues +#Runs every Monday and Thursday at 9:00 A.M +on: + schedule: + - cron: "0 9 * * 1,4" +env: + inactiveIntervalDays: ${{ vars.MONITORING_INACTIVE_INTERVAL_DAYS }} +jobs: + retrieve-inactive-issues: + runs-on: ubuntu-latest + permissions: + issues: write + outputs: + issues: ${{ steps.filter-inactive-issues.outputs.result }} + steps: + - name: Check environment + run: | + if [ -z $inactiveIntervalDays ]; then + echo "::error::'MONITORING_INACTIVE_INTERVAL_DAYS' environment variable is not set" + exit 1 + fi + - uses: actions/checkout@v3 + if: success() + - name: Filter inactive issues + id: filter-inactive-issues + uses: actions/github-script@v6 + with: + script: | + const script = require('./.github/workflows/scripts/filterInactiveIssues.js'); + return await script({github, context, core}); + notify-inactive-issues: + runs-on: ubuntu-latest + needs: retrieve-inactive-issues + if: ${{ needs.retrieve-inactive-issues.outputs.issues }} + strategy: + matrix: + issues: ${{ fromJSON(needs.retrieve-inactive-issues.outputs.issues) }} + steps: + - name: Notify MS Teams channel + id: notify-ms-teams + uses: simbo/msteams-message-card-action@latest + with: + webhook: ${{ secrets.COMMUNITY_EVENTS_WEBHOOK_URL }} + title: Inactive Issue Detected + message: | + It's been ${{ env.inactiveIntervalDays }} days since issue number ${{ matrix.issues.number }} has received an update. ${{ matrix.issues.assignee }}, please provide an update soon. + buttons: | + View Issue on GitHub ${{ matrix.issues.url }} diff --git a/.github/workflows/monitor-issues-in-voting.yaml b/.github/workflows/monitor-issues-in-voting.yaml new file mode 100644 index 00000000000..929310d1475 --- /dev/null +++ b/.github/workflows/monitor-issues-in-voting.yaml @@ -0,0 +1,58 @@ +name: Monitor issues on voting status +#Runs the first day of each month at 10 A.M +on: + schedule: + - cron: "0 10 1 * *" + +jobs: + select-top-voted-issue: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v3 + - name: Select the most voted enhancement + id: select-top-voted-issue + uses: actions/github-script@v6 + with: + script: | + const script = require('./.github/workflows/scripts/selectMostVotedIssue.js') + return await script({github, context, core}) + - name: Notify MS Teams channel + id: notify-ms-teams + if: ${{ success() && steps.select-top-voted-issue.outputs.result }} + uses: simbo/msteams-message-card-action@latest + env: + issueTitle: ${{ fromJSON(steps.select-top-voted-issue.outputs.result).title }} + issueURL: ${{ fromJSON(steps.select-top-voted-issue.outputs.result).url }} + assignee: ${{ fromJSON(steps.select-top-voted-issue.outputs.result).assignee }} + with: + webhook: ${{ secrets.COMMUNITY_EVENTS_WEBHOOK_URL }} + title: An Enhancement Proposal Issue has been selected the top-voted issue! + message: ${{ env.issueTitle }} assigned to ${{ env.assignee }} + buttons: | + View Issue on GitHub ${{ env.issueURL }} + close-forgotten-voting-issues: + runs-on: ubuntu-latest + permissions: + issues: write + needs: select-top-voted-issue + env: + maximumVotingThreshold: ${{ vars.MAX_VOTING_THRESHOLD_DAYS }} + steps: + - name: Check environment + run: | + if [ -z $maximumVotingThreshold ]; then + echo "::error::'MAX_VOTING_THRESHOLD_DAYS' environment variable is not set" + exit 1 + fi + - uses: actions/checkout@v3 + if: success() + - name: Close "forgotten" enhancement requests + id: close-forgotten-enhancements + if: success() + uses: actions/github-script@v6 + with: + script: | + const script = require('./.github/workflows/scripts/closeForgottenEnhancements.js') + script({github, context, core}) diff --git a/.github/workflows/scripts/closeForgottenEnhancements.js b/.github/workflows/scripts/closeForgottenEnhancements.js new file mode 100644 index 00000000000..0638b3a3e76 --- /dev/null +++ b/.github/workflows/scripts/closeForgottenEnhancements.js @@ -0,0 +1,60 @@ +module.exports = async ({github, context, core}) => { + const {owner, repo} = context.repo; + // Query all GH issues for Voting + + const votingLabel = "Status: Voting"; + let response = await github.rest.issues.listForRepo({ + owner, + repo, + labels: votingLabel, + state: 'open', + }); + if (response.data.length === 0) { + core.debug('No issues marked for voting found. Exiting.'); + return; + } + const votingThreshold = process.env.maximumVotingThreshold; + const parsedDays = parseFloat(votingThreshold); + + let now = new Date().getTime(); + for (let issue of response.data) { + core.debug(`Processing issue #${issue.number}`); + core.debug(`Issue was created ${issue.created_at}`); + + let createdDate = new Date(issue.created_at).getTime(); + let daysSinceCreated = (now - createdDate) / 1000 / 60 / 60 / 24; + let reactions = issue.reactions['+1']; + + core.debug(`Issue +1 reactions count is ${reactions}`); + + if (reactions < 2 && daysSinceCreated > parsedDays) { + core.debug(`Closing #${issue.number} because it hasn't received enough votes after ${parsedDays} days`); + + const message = `Greetings, + This issue has been open for community voting for more than ${parsedDays} days and sadly it hasn't received enough votes to be considered for its implementation according to our community policies. + As there is not enough interest from the community we'll proceed to close this issue.`; + + await github.rest.issues.createComment({ + owner : owner, + repo : repo, + issue_number: issue.number, + body: message + }); + + await github.rest.issues.update({ + owner: owner, + repo: repo, + issue_number: issue.number, + labels: [], + state: 'closed' + }); + + await github.rest.issues.lock({ + owner : owner, + repo : repo, + issue_number : issue.number, + lock_reason : 'resolved' + }); + } + } +} diff --git a/.github/workflows/scripts/filterInactiveIssues.js b/.github/workflows/scripts/filterInactiveIssues.js new file mode 100644 index 00000000000..33923b2532c --- /dev/null +++ b/.github/workflows/scripts/filterInactiveIssues.js @@ -0,0 +1,43 @@ +module.exports = async ({github, context, core}) => { + const { owner, repo } = context.repo; + const openLabel = "Status: Open"; + + const parsedDays = process.env.inactiveIntervalDays; + const thresholdInMillis = parsedDays * 24 * 60 * 60 * 1000; + + // Query all GH issues that are open + const response = await github.rest.issues.listForRepo({ + owner, + repo, + labels: openLabel, + state: "open", + }); + core.debug(`Inactive interval days is set to ${parsedDays}`); + + let inactiveIssues = []; + for(let issue of response.data){ + //Get issue events, which are returned by creation date in descending order + const eventResponse = await github.rest.issues.listEvents({ + owner, + repo, + issue_number : issue.number + }); + //Filter which events correspond to the 'labeled' event in which the `Status: Open` label was added + let lastOpenEvent = eventResponse.data.filter((event) => event.event === 'labeled' && event.label.name === openLabel)[0]; + + //If the event date is beyond the threshold date, the issue is added to the result array + if((new Date().getTime() - new Date(lastOpenEvent.created_at).getTime()) > thresholdInMillis){ + inactiveIssues.push({ + number : issue.number, + title : issue.title, + url: issue.html_url, + assignee: issue.assignees.length ? issue.assignees[0].login : '???' + }); + } + } + + core.debug(`${inactiveIssues.length} issues detected to be inactive`); + if (inactiveIssues.length > 0) { + return inactiveIssues; + } +} diff --git a/.github/workflows/scripts/selectMostVotedIssue.js b/.github/workflows/scripts/selectMostVotedIssue.js new file mode 100644 index 00000000000..440b5372b44 --- /dev/null +++ b/.github/workflows/scripts/selectMostVotedIssue.js @@ -0,0 +1,76 @@ +module.exports = async ({github, context, core}) => { + let { owner, repo } = context.repo; + + const openLabel = "Status: Open"; + const votingLabel = "Status: Voting"; + + // Query all GH issues for Voting + const response = await github.rest.issues.listForRepo({ + owner, + repo, + labels: votingLabel, + state: 'open', + direction: 'desc', + }); + + //response has all the issues labeled with Voting. + if (response.data.length === 0) { + core.debug('No issues marked for voting found. Exiting.'); + return; + } + //filter issues with at least 2 votes. + response.data = response.data.filter((issue) => issue.reactions['+1'] > 1) + if (response.data.length === 0) { + core.debug('No issues with more than 2 votes found. Exiting'); + return; + } + + let mostVotes = 0; + let selectedIssue = 0; + let oldestDate = null; + + for (const issue of response.data) { + core.debug(`Processing issue #${issue.number}`); + core.debug(`Number of +1 reactions ${issue.reactions['+1']}`); + core.debug(`Issue was created ${issue.created_at}`); + + let votes = issue.reactions['+1']; + let createdDate = new Date(issue.created_at).getTime(); + + if (oldestDate === null) { + oldestDate = createdDate; + selectedIssue = issue; + mostVotes = votes; + } + if ((votes >= mostVotes) && (createdDate < oldestDate)) { + mostVotes = votes; + selectedIssue = issue; + } + } + core.debug(`Highest votes is ${mostVotes}`); + core.debug(`Final issue selected for enhancement is #${selectedIssue.number} created on ${selectedIssue.created_at}`); + + let message = `Greetings, + This enhancement request has been selected by the Payara Community as the most voted enhancement of this month and + thus will be escalated to our product development backlog.`; + + await github.rest.issues.createComment({ + owner : owner, + repo : repo, + issue_number: selectedIssue.number, + body: message + }); + await github.rest.issues.update({ + owner : owner, + repo : repo, + issue_number: selectedIssue.number, + labels : [openLabel] + }); + + return { + number : selectedIssue.number, + title : selectedIssue.title, + url: selectedIssue.html_url, + assignee: selectedIssue.assignees.length ? selectedIssue.assignees[0].login : null + }; +}