diff --git a/plugins/filters/suggestIssues/LICENSE b/plugins/filters/suggestIssues/LICENSE new file mode 100644 index 00000000..4af7cd03 --- /dev/null +++ b/plugins/filters/suggestIssues/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 LinearB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/filters/suggestIssues/README.md b/plugins/filters/suggestIssues/README.md new file mode 100644 index 00000000..6fb6c6e1 --- /dev/null +++ b/plugins/filters/suggestIssues/README.md @@ -0,0 +1,24 @@ + +--8<-- "plugins/filters/suggestIssues/reference.md" + + +??? note "Plugin Code: suggestIssues" + ```javascript + --8<-- "plugins/filters/suggestIssues/index.js" + ``` +
+ + +
+ + +??? example "gitStream CM Example: suggestIssues" + ```yaml+jinja + --8<-- "plugins/filters/suggestIssues/suggestIssues.cm" + ``` +
+ + +
+ +[Download Source Code](https://github.com/linear-b/gitstream/tree/main/plugins/filters/suggestIssues) diff --git a/plugins/filters/suggestIssues/index.js b/plugins/filters/suggestIssues/index.js new file mode 100644 index 00000000..6140efc2 --- /dev/null +++ b/plugins/filters/suggestIssues/index.js @@ -0,0 +1,69 @@ +/** + * @module suggestIssues + * @description Fetches ticket recommendations based on given pull request details. + * @param {object} pr - The pull request object containing title, author, and created_at properties. + * @param {object} branch - The branch object containing the branch name. + * @param {string} apiKey - The API key used to authenticate requests. + * @returns {Array} Returns an array of suggested issues related to the current Pull Request. + * @example {{ pr | suggestIssues(branch, env.LINEARB_TOKEN) }} + * @license MIT +**/ + +const suggestIssues = async (pr, branch, apiKey, callback) => { + const url = + "https://public-api.linearb.io/api/v1/inference/get_ticket_recommendation"; + + const requestData = { + request_id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // <-- local UUID per call + pull_request: { + title: pr.title, // PR title + issuer_name: pr.author, // PR author + created_at: pr.created_at, // PR creation date + branch_name: branch.name, // PR branch name + }, + }; + + const result = await fetch(url, { + method: "POST", + headers: { + "x-api-key": apiKey, + "Content-Type": "application/json", + "accept": "application/json", + }, + body: JSON.stringify(requestData), + }) + .then((response) => response.json()) + .then((data) => data) + .catch((error) => console.log("Error:", error)); + + if (result && result.recommendations && result.recommendations.jira_tickets) { + // Extract the first 3 issues + const issues = result.recommendations.jira_tickets.slice(0, 3); + + // Map to the desired object format containing the issue URL and issue title + const issuesMarkdown = issues + .map((issue) => ({ + url: issue.url, + title: issue.title.replace(/\n/g, "").trim(), + key: issue.issue_provider_key, + score: issue.similarity_score, + })) + // Map to the desired object format containing the issue URL and issue title + .map((issue) => `- [ ] [${issue.key}](${issue.url}) ${issue.title} _(score: ${issue.score.toFixed(2)})_ `) + .join("\\n"); + console.log("suggestedIssues:", {issuesMarkdown}); + + return callback(null, issuesMarkdown); + } else if (result && result.recommendations && Array.isArray(result.recommendations.jira_tickets) && result.recommendations.jira_tickets.length === 0) { + console.log("No issues found.", JSON.stringify(result, null, 2)); + return callback(null, "No related issues found. The pull request title and the author were used to search the Jira board, but no good match was found."); + } else { + console.log("Invalid response structure:", JSON.stringify(result, null, 2) ); + return callback(null, "Error: Service returned an invalid response structure."); + } +}; + +module.exports = { + async: true, + filter: suggestIssues +} \ No newline at end of file diff --git a/plugins/filters/suggestIssues/reference.md b/plugins/filters/suggestIssues/reference.md new file mode 100644 index 00000000..c6795030 --- /dev/null +++ b/plugins/filters/suggestIssues/reference.md @@ -0,0 +1,22 @@ + + +## suggestIssues +A gitStream plugin to suggest issues to link to the PR + +![Example PR description](screenshots/suggestIssues.png) + +**Returns**: \[String\] - Returns a list of suggested issues +**License**: MIT + +| Param | Type | Description | +| ------ | -------- | ------------------------------------------------ | +| pr | `Object` | The pull request object from gitStream's context | +| branch | Object | The branch object from gitStream's context | +| apiKey | `string` | The API key used to authenticate requests. | + + +**Example** + +```yaml +{{ pr | suggestIssues(env.LINEARB_TOKEN) }} +``` diff --git a/plugins/filters/suggestIssues/suggestIssues.cm b/plugins/filters/suggestIssues/suggestIssues.cm new file mode 100644 index 00000000..4cba4d00 --- /dev/null +++ b/plugins/filters/suggestIssues/suggestIssues.cm @@ -0,0 +1,46 @@ +# -*- mode: yaml -*- + +manifest: + version: 1.0 + +automations: + # Add comment that suggests tickets for this PR when there is no linked + # ticket in the title or description + suggest_issues: + on: + - label_added + if: + - {{ pr.labels | match(term='suggest-ticket') | some }} + run: + - action: add-comment@v1 + args: + comment: | + Select one of the suggested issues to link to this PR. Once you make a selection, gitStream will automatically update the PR title and description with the selected issue key (taking into account issues opened up to the last 2 hours). + + {{ pr | suggestedIssues(branch, env.TICKET_SUGGESTION_TOKEN) }} + + *✨ This list was created using LinearB’s AI service* + + # If the PR author has indicated that they used a specific issue, + # add it to the PR description and title + add_selected_issue_to_pr: + if: + - {{ pr.comments | filter(attr='commenter', term='gitstream-cm') | filter (attr='content', regex=r/\- \[x\]/) | some }} + - {{ not (has.jira_ticket_in_title or has.jira_ticket_in_desc) }} + run: + - action: update-title@v1 + args: + concat_mode: prepend + title: | + {{ selected_issue_key }} - + - action: update-description@v1 + args: + concat_mode: prepend + description: | + Jira issue: {{ selected_issue_key }} + +selected_issue_key: {{ pr.comments | filter(attr='commenter', term='gitstream-cm') | map(attr='content') | filter (regex=r/\- \[x\].*/) | first | capture(regex=r/\b[A-Za-z]+-\d+\b/) }} + +has: + jira_ticket_in_title: {{ pr.title | includes(regex=r/\b[A-Za-z]+-\d+\b/) }} + jira_ticket_in_desc: {{ pr.description | includes(regex=r/atlassian.net\/browse\/\w{1,}-\d{3,4}/) }} \ No newline at end of file