diff --git a/.github/workflows/issue-comment-trigger.yaml b/.github/workflows/issue-comment-trigger.yaml new file mode 100644 index 0000000000..31ab11e6c8 --- /dev/null +++ b/.github/workflows/issue-comment-trigger.yaml @@ -0,0 +1,13 @@ +name: Issue Comment Trigger +on: + issue_comment: + types: + - created + +jobs: + Add-Comment-To-Prework-Issue: + if: ${{ github.event_name == 'issue_comment' && github.event.action == 'created'}} + permissions: + issues: write + uses: ./.github/workflows/prework-issue-comment-reusable-workflow.yaml + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/issue-trigger.yml b/.github/workflows/issue-trigger.yml index f2895c5586..9b902572ac 100644 --- a/.github/workflows/issue-trigger.yml +++ b/.github/workflows/issue-trigger.yml @@ -1,7 +1,7 @@ name: Issue Trigger on: issues: - types: [opened, transferred, assigned, labeled, unlabeled] + types: [opened, closed, transferred, assigned, unassigned, labeled, unlabeled] jobs: # Adds newly created issues onto project board in the default column 'New Issue Approval' @@ -111,3 +111,11 @@ jobs: script: | const script = require('./github-actions/trigger-issue/feature-branch-comment/hide-feature-branch-comment.js') script({g: github, c: context}) + +# Adds a comment to actor's prework issue + Add-Comment-To-Prework-Issue: + if: "${{ github.event.action == 'opened' || github.event.action == 'closed' || github.event.action == 'assigned' || github.event.action == 'unassigned'}}" + permissions: + issues: write + uses: ./.github/workflows/prework-issue-comment-reusable-workflow.yaml + secrets: inherit diff --git a/.github/workflows/prework-issue-comment-reusable-workflow.yaml b/.github/workflows/prework-issue-comment-reusable-workflow.yaml new file mode 100644 index 0000000000..4d65ddc504 --- /dev/null +++ b/.github/workflows/prework-issue-comment-reusable-workflow.yaml @@ -0,0 +1,72 @@ +name: Find Prework Issue and Comment User Activity + +on: + workflow_call: + +jobs: + Add-Comment-To-Prework-Issue-Reusable: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@v4 + + - name: Get activity detail + id: get-activity-detail + uses: actions/github-script@v7 + with: + script: | + const script = require('./github-actions/prework-issue-reusable-workflow/get-activity-detail.js') + const contributor = script({g: github, c: context}) + return contributor + + - name: Get prework issue + id: get-prework-issue + uses: actions/github-script@v7 + with: + script: | + const script = require('./github-actions/prework-issue-reusable-workflow/get-prework-issue.js') + const activityDetail = ${{steps.get-activity-detail.outputs.result}} + if (!activityDetail) { + return false + } + return script({g: github, c: context}, activityDetail) + + - name: Update prework issue status + id: update-prework-issue-status + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} + script: | + const script = require('./github-actions/prework-issue-reusable-workflow/update-prework-issue-status.js') + const issue = ${{steps.get-prework-issue.outputs.result}} + if (issue) { + return script({g: github, c: context}, issue) + } + return false + + # Only move issue if issue was closed and is reopened + - name: Move issue + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} + # NOTE: The projectColumnId is hard-coded + script: | + const projectColumnId = "MDEzOlByb2plY3RDb2x1bW43MTk4MjI4" + const issue = ${{steps.update-prework-issue-status.outputs.result}} + const script = require('./github-actions/utils/update-issue-project-card.js') + if (issue) { + const issueCardId = issue.projectCards.nodes[0].id + script(issueCardId, projectColumnId, github) + } + + - name: Leave comment on issue + uses: actions/github-script@v7 + with: + script: | + const script = require('./github-actions/prework_issue_comment/add-prework-comment.js') + const issue = ${{steps.get-prework-issue.outputs.result}} + const activityDetail = ${{steps.get-activity-detail.outputs.result}} + if (issue && activityDetail) { + script({ g: github, c: context}, issue.number, activityDetail) + } \ No newline at end of file diff --git a/.github/workflows/pull-request-review-comment-trigger.yaml b/.github/workflows/pull-request-review-comment-trigger.yaml new file mode 100644 index 0000000000..6248ca4bd1 --- /dev/null +++ b/.github/workflows/pull-request-review-comment-trigger.yaml @@ -0,0 +1,12 @@ +name: Pull Request Review Comment Trigger +on: + pull_request_review_comment: + types: + - created +jobs: + Add-Comment-To-Prework-Issue: + if: ${{ github.event_name == 'pull_request_review_comment' && github.event.action == 'created'}} + permissions: + issues: write + uses: ./.github/workflows/prework-issue-comment-reusable-workflow.yaml + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/pull-request-review-trigger.yaml b/.github/workflows/pull-request-review-trigger.yaml new file mode 100644 index 0000000000..bf127e56df --- /dev/null +++ b/.github/workflows/pull-request-review-trigger.yaml @@ -0,0 +1,12 @@ +name: Pull Request Review Trigger +on: + pull_request_review: + types: + - submitted +jobs: + Add-Comment-To-Prework-Issue: + if: ${{ github.event_name == 'pull_request_review' && github.event.action == 'submitted'}} + permissions: + issues: write + uses: ./.github/workflows/prework-issue-comment-reusable-workflow.yaml + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/pull-request-trigger.yml b/.github/workflows/pull-request-trigger.yml index 3a11ba65fc..4218c46485 100644 --- a/.github/workflows/pull-request-trigger.yml +++ b/.github/workflows/pull-request-trigger.yml @@ -64,4 +64,11 @@ jobs: project: Project Board column: 'PR Needs review (Automated Column, do not place items here manually)' repo-token: ${{ secrets.HACKFORLA_BOT_PA_TOKEN }} - action: delete \ No newline at end of file + action: delete + + Add-Comment-To-Prework-Issue: + if: ${{ github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'closed')}} + permissions: + issues: write + uses: ./.github/workflows/prework-issue-comment-reusable-workflow.yaml + secrets: inherit \ No newline at end of file diff --git a/github-actions/prework-issue-reusable-workflow/add-prework-issue-comment.js b/github-actions/prework-issue-reusable-workflow/add-prework-issue-comment.js new file mode 100644 index 0000000000..1ceff2b96f --- /dev/null +++ b/github-actions/prework-issue-reusable-workflow/add-prework-issue-comment.js @@ -0,0 +1,36 @@ +// Import modules +const postComment = require("../utils/post-issue-comment") + +// Global variables +var github +var context + +/** + * @description - This function is the entry point into the javascript file, it leaves a comment on the prework issue + * @param {Object} g - github object + * @param {Object} c - context object + * @param {Number} issueNumber - issue + * @param {Object} activityDetail - details of the activity performed + */ +async function main({ g, c }, issueNumber, activityDetail) { + github = g + context = c + const comment = await makeComment(activityDetail) + if (comment !== null) { + await postComment(issueNumber, comment, github, context) + } +} + +/** + * @description - This function makes the comment + * @param {Object} activityDetail - details of the activity performed + * @returns {Promise} - Comment to be posted with the issue label event actor's name + */ + +async function makeComment(activityDetail) { + const comment = `${activityDetail.activityObject} has been ${activityDetail.action} by @${activityDetail.contributor} +` + return comment +} + +module.exports = main diff --git a/github-actions/prework-issue-reusable-workflow/get-activity-detail.js b/github-actions/prework-issue-reusable-workflow/get-activity-detail.js new file mode 100644 index 0000000000..852f67074f --- /dev/null +++ b/github-actions/prework-issue-reusable-workflow/get-activity-detail.js @@ -0,0 +1,136 @@ +// Import modules +var fs = require("fs") + +// Global variables +var github +var context + +/** + * @description - This function is the entry point into the javascript file, + * It will determine the eventName and return the proper activity detail + * @param {Object} g - github object + * @param {Object} c - context object + */ +async function main({ g, c }) { + github = g + context = c + + switch (context.eventName) { + case "issue_comment": + return await getIssueCommentEventType(context) + case "issues": + return await getIssueEventType(context) + case "pull_request": + return await getPullRequestEventType(context) + case "pull_request_review": + return await getPullRequestReviewEventType(context) + case "pull_request_review_comment": + return await getPullRequestReviewCommentEventType(context) + default: + // Handle cases where eventName doesn't match any known event type + console.log(`Unknown event name: ${context.eventName}`) + } +} + +/** + * Retrieves detailed information about an issue event based on the action performed. + * @param {*} context + * @returns - An object containing the activity detail: + * - contributor + * - action + * - object the activity was performed on (issue, issue_comment, pr, pr_comment, pr_review) + * + */ +async function getIssueEventType(context) { + const activityDetail = { + contributor: "", + action: context.payload.action, + activityObject: `Issue #${context.payload.issue.number}`, + } + if (context.payload.action == "opened") { + activityDetail.contributor = context.payload.issue.user.login + } else if (context.payload.action == "closed") { + // NOTE: context.payload.issue.assignee.login can be null if issue is not assigned and marked closed + activityDetail.contributor = context.payload.issue.assignee.login + } else { + // on unassigned event, the `issue.assignee` is null. + activityDetail.contributor = context.payload.assignee.login + } + return activityDetail +} + +/** + * Retrieves detailed information about an issue event based on the action performed. + * @param {*} context + * @returns - An object containing the activity detail: + * - contributor + * - action + * - object the activity was performed on (issue, issue_comment, pr, pr_comment, pr_review) + * + */ +async function getIssueCommentEventType(context) { + const activityDetail = { + contributor: context.payload.comment.user.login, + action: context.payload.action, + activityObject: context.payload.comment.url, + } + return activityDetail +} + +/** + * Retrieves detailed information about an issue event based on the action performed. + * @param {*} context + * @returns - An object containing the activity detail: + * - contributor + * - action + * - object the activity was performed on (issue, issue_comment, pr, pr_comment, pr_review) + * + */ +async function getPullRequestEventType(context) { + const activityDetail = { + contributor: context.payload.pull_request.user.login, + action: context.payload.action, + activityObject: `PR #${context.payload.pull_request.number}`, + } + return activityDetail +} + +/** + * Retrieves detailed information about an issue event based on the action performed. + * @param {*} context + * @returns - An object containing the activity detail: + * - contributor + * - action + * - object the activity was performed on (issue, issue_comment, pr, pr_comment, pr_review) + * + */ +async function getPullRequestReviewEventType(context) { + // also achievable by the context.sender - user who did this event + const activityDetail = { + contributor: context.payload.review.user.login, + action: context.payload.action, + activityObject: `PR #${context.payload.pull_request.number}`, + } + return activityDetail +} + +/** + * Retrieves detailed information about an issue event based on the action performed. + * @param {*} context + * @returns - An object containing the activity detail: + * - contributor + * - action + * - object the activity was performed on (issue, issue_comment, pr, pr_comment, pr_review) + * + */ +async function getPullRequestReviewCommentEventType(context) { + // also achievable by the context.sender - user who did this event + const activityDetail = { + contributor: context.payload.comment.user.login, + action: context.payload.action, + activityObject: `${context.payload.comment.url}`, + } + return activityDetail +} + +module.exports = main diff --git a/github-actions/prework-issue-reusable-workflow/get-prework-issue.js b/github-actions/prework-issue-reusable-workflow/get-prework-issue.js new file mode 100644 index 0000000000..f2f953ed41 --- /dev/null +++ b/github-actions/prework-issue-reusable-workflow/get-prework-issue.js @@ -0,0 +1,29 @@ +// Import modules +var getIssueByLabel = require("../utils/get-issue-by-label") + +// Global variables +var github +var context + +/** + * @description - This function is the entry point into the javascript file, + * it will find the prework issue + * + * @param {Object} g - github object + * @param {Object} c - context object + * @param {Object} activityDetail - person who triggered the workflow + */ +async function main({ g, c }, activityDetail) { + github = g + context = c + let labels = ["Complexity: Prework"] + const issue = await getIssueByLabel( + activityDetail.contributor, + labels, + github, + context + ) + return issue +} + +module.exports = main diff --git a/github-actions/prework-issue-reusable-workflow/update-prework-issue-status.js b/github-actions/prework-issue-reusable-workflow/update-prework-issue-status.js new file mode 100644 index 0000000000..56d4fb3e4d --- /dev/null +++ b/github-actions/prework-issue-reusable-workflow/update-prework-issue-status.js @@ -0,0 +1,29 @@ +// Import modules +var reopenIssue = require("../utils/reopen-issue") + +// Global variables +var github +var context + +/** + * @description - This function is the entry point into the javascript file, it will reopen the issue + * @param {Object} g - github object + * @param {Object} c - context object + * @param {Object} issue - the issue to update + */ +async function main({ g, c }, issue) { + github = g + context = c + + // TODO value is hardcoded + // Project Number =1 , for HFLA project number = 7 + console.log(issue) + if (issue.closed == true) { + const result = await reopenIssue(issue.id, github) + console.log(result) + return result.reopenIssue.issue + } + return false +} + +module.exports = main diff --git a/github-actions/utils/get-issue-by-label.js b/github-actions/utils/get-issue-by-label.js new file mode 100644 index 0000000000..0067bf7e0b --- /dev/null +++ b/github-actions/utils/get-issue-by-label.js @@ -0,0 +1,50 @@ +/** + * Gets issue by label + * @param {String} actor - the creator of the issue + * @param {Array} labels - the labelS we want to filter by + */ +async function getIssueByLabel(actor, labels, github, context) { + try { + const [owner, name] = context.payload.repository.full_name.split("/") + let results = await github.graphql(query, { + actor: actor, + labels: labels, + owner: owner, + repository: name, + }) + let issue = results.repository.issues.nodes[0] + return issue + } catch (err) { + console.error(err) + throw new Error(err) + } +} + +/** + * GraphQL query template to use to execute the search. + */ +const query = ` +query ($actor: String!, $labels: [String!]!, $owner: String!, $repository: String!) { + repository (owner: $owner, name: $repository) { + issues (first: 10, filterBy: {createdBy: $actor labels: $labels}) { + nodes { + __typename + ... on Issue { + title + closed + number + url + id + projectCards { + nodes { + id + } + } + } + } + } +} +} +` + +module.exports = getIssueByLabel diff --git a/github-actions/utils/reopen-issue.js b/github-actions/utils/reopen-issue.js new file mode 100644 index 0000000000..db6340b054 --- /dev/null +++ b/github-actions/utils/reopen-issue.js @@ -0,0 +1,35 @@ +/** + * Reopen a closed issue + * @param {Number} issueId - the issue that should be reopened + */ +async function reopenIssue(issueId, github) { + try { + const response = await github.graphql(mutation, { + issueId: issueId, + }) + return response + } catch (err) { + throw new Error(err) + } +} + +/** + * GraphQL query template to use to execute the mutation. + */ +const mutation = ` +mutation ($issueId: ID!) { + reopenIssue (input: { issueId: $issueId}) { + issue { + title + state + projectCards { + nodes { + id + } + } + } + } +} +` + +module.exports = reopenIssue diff --git a/github-actions/utils/update-issue-project-card.js b/github-actions/utils/update-issue-project-card.js new file mode 100644 index 0000000000..8c64fb80ed --- /dev/null +++ b/github-actions/utils/update-issue-project-card.js @@ -0,0 +1,32 @@ +/** + * Move Issue to In Progress Column + * @param {String} issueCardId - the issue card ID + * @param {String} projectColumnId - the project column ID + */ +async function updateIssueProjectCard(issueCardId, projectColumnId, github) { + try { + await github.graphql(mutation, { + cardId: issueCardId, + columnId: projectColumnId, + }) + } catch (err) { + throw new Error(err) + } +} + +/** + * GraphQL query template to use to move project card to In Progress Column + */ +const mutation = ` +mutation ($cardId: ID! $columnId: ID!) { + moveProjectCard (input: {cardId: $cardId columnId: $columnId}) { + cardEdge { + node { + resourcePath + } + } + } +} +` + +module.exports = updateIssueProjectCard