|
|
@@ -0,0 +1,222 @@
|
|
|
+/** Check that there are valid JIRA links in MR desctiption.
|
|
|
+ *
|
|
|
+ * This check extracts the "Related" section from the MR description and
|
|
|
+ * searches for JIRA ticket references in the format "Closes [JIRA ticket key]".
|
|
|
+ *
|
|
|
+ * It then extracts the closing GitHub links from the corresponding JIRA tickets and
|
|
|
+ * checks if the linked GitHub issues are still in open state.
|
|
|
+ *
|
|
|
+ * Finally, it checks if the required GitHub closing links are present in the MR's commit messages.
|
|
|
+ *
|
|
|
+ */
|
|
|
+module.exports = async function () {
|
|
|
+ const axios = require("axios");
|
|
|
+ const mrDescription = danger.gitlab.mr.description;
|
|
|
+ const mrCommitMessages = danger.gitlab.commits.map(
|
|
|
+ (commit) => commit.message
|
|
|
+ );
|
|
|
+
|
|
|
+ let partMessages = []; // Create a blank field for future records of individual issues
|
|
|
+
|
|
|
+ // Parse section "Related" from MR Description
|
|
|
+ const sectionRelated = extractSectionRelated(mrDescription);
|
|
|
+
|
|
|
+ if (
|
|
|
+ !sectionRelated.header || // No section Related in MR description or ...
|
|
|
+ !/\s[A-Z]+-[0-9]+\s/.test(sectionRelated.content) // no Jira links in section Related
|
|
|
+ ) {
|
|
|
+ return message(
|
|
|
+ "Please consider adding references to JIRA issues in the `Related` section of the MR description."
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get closing (only) JIRA tickets
|
|
|
+ const jiraTickets = findClosingJiraTickets(sectionRelated.content);
|
|
|
+
|
|
|
+ for (const ticket of jiraTickets) {
|
|
|
+ ticket.jiraUIUrl = `https://jira.espressif.com:8443/browse/${ticket.ticketName}`;
|
|
|
+
|
|
|
+ if (!ticket.correctFormat) {
|
|
|
+ partMessages.push(
|
|
|
+ `- closing ticket \`${ticket.record}\` seems to be in the incorrect format. The correct format is for example \`- Closes JIRA-123\``
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get closing GitHub issue links from JIRA tickets
|
|
|
+ const closingGithubLink = await getGitHubClosingLink(ticket.ticketName);
|
|
|
+ if (closingGithubLink) {
|
|
|
+ ticket.closingGithubLink = closingGithubLink;
|
|
|
+ } else if (closingGithubLink === null) {
|
|
|
+ partMessages.push(
|
|
|
+ `- the Jira issue number [\`${ticket.ticketName}\`](${ticket.jiraUIUrl}) seems to be invalid (please check if the ticket number is correct)`
|
|
|
+ );
|
|
|
+ continue; // Handle unreachable JIRA tickets; skip the following checks
|
|
|
+ } else {
|
|
|
+ continue; // Jira ticket have no GitHub closing link; skip the following checks
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get still open GitHub issues
|
|
|
+ const githubIssueStatusOpen = await isGithubIssueOpen(
|
|
|
+ ticket.closingGithubLink
|
|
|
+ );
|
|
|
+ ticket.isOpen = githubIssueStatusOpen;
|
|
|
+ if (githubIssueStatusOpen === null) {
|
|
|
+ // Handle unreachable GitHub issues
|
|
|
+ partMessages.push(
|
|
|
+ `- the GitHub issue [\`${ticket.closingGithubLink}\`](${ticket.closingGithubLink}) does not seem to exist on GitHub (referenced from JIRA ticket [\`${ticket.ticketName}\`](${ticket.jiraUIUrl}) )`
|
|
|
+ );
|
|
|
+ continue; // skip the following checks
|
|
|
+ }
|
|
|
+
|
|
|
+ // Search in commit message if there are all GitHub closing links (from Related section) for still open GH issues
|
|
|
+ if (ticket.isOpen) {
|
|
|
+ if (
|
|
|
+ !mrCommitMessages.some((item) =>
|
|
|
+ item.includes(`Closes ${ticket.closingGithubLink}`)
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ partMessages.push(
|
|
|
+ `- please add \`Closes ${ticket.closingGithubLink}\` to the commit message`
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create report / DangerJS check feedback if issues with Jira links found
|
|
|
+ if (partMessages.length) {
|
|
|
+ createReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This function takes in a string mrDescription which contains a Markdown-formatted text
|
|
|
+ * related to a Merge Request (MR) in a GitLab repository. It searches for a section titled "Related"
|
|
|
+ * and extracts the content of that section. If the section is not found, it returns an object
|
|
|
+ * indicating that the header and content are null. If the section is found but empty, it returns
|
|
|
+ * an object indicating that the header is present but the content is null. If the section is found
|
|
|
+ * with content, it returns an object indicating that the header is present and the content of the
|
|
|
+ * "Related" section.
|
|
|
+ *
|
|
|
+ * @param {string} mrDescription - The Markdown-formatted text related to the Merge Request.
|
|
|
+ * @returns {{
|
|
|
+ * header: string | boolean | null,
|
|
|
+ * content: string | null
|
|
|
+ * }} - An object containing the header and content of the "Related" section, if present.
|
|
|
+ */
|
|
|
+
|
|
|
+ function extractSectionRelated(mrDescription) {
|
|
|
+ const regexSectionRelated = /## Related([\s\S]*?)(?=## |$)/;
|
|
|
+ const sectionRelated = mrDescription.match(regexSectionRelated);
|
|
|
+ if (!sectionRelated) {
|
|
|
+ return { header: null, content: null }; // Section "Related" is missing
|
|
|
+ }
|
|
|
+
|
|
|
+ const content = sectionRelated[1].replace(/(\r\n|\n|\r)/gm, ""); // Remove empty lines
|
|
|
+ if (!content.length) {
|
|
|
+ return { header: true, content: null }; // Section "Related" is present, but empty
|
|
|
+ }
|
|
|
+
|
|
|
+ return { header: true, content: sectionRelated[1] }; // Found section "Related" with content
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Finds all JIRA tickets that are being closed in the given sectionRelatedcontent.
|
|
|
+ * The function searches for lines that start with - Closes and have the format Closes [uppercase letters]-[numbers].
|
|
|
+ * @param {string} sectionRelatedcontent - A string that contains lines with mentions of JIRA tickets
|
|
|
+ * @returns {Array} An array of objects with ticketName property that has the correct format
|
|
|
+ */
|
|
|
+
|
|
|
+ function findClosingJiraTickets(sectionRelatedcontent) {
|
|
|
+ let closingTickets = [];
|
|
|
+ const lines = sectionRelatedcontent.split("\n");
|
|
|
+ for (const line of lines) {
|
|
|
+ if (!line.startsWith("- Closes")) {
|
|
|
+ continue; // Not closing-type ticket, skip
|
|
|
+ }
|
|
|
+
|
|
|
+ const correctJiraClosingLinkFormat = /^- Closes [A-Z]+\-\d+$/;
|
|
|
+ if (!correctJiraClosingLinkFormat.test(line)) {
|
|
|
+ closingTickets.push({
|
|
|
+ record: line,
|
|
|
+ ticketName: line.match(/[A-Z]+\-\d+/)[0],
|
|
|
+ correctFormat: false,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ closingTickets.push({
|
|
|
+ record: line,
|
|
|
+ ticketName: line.match(/[A-Z]+\-\d+/)[0],
|
|
|
+ correctFormat: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return closingTickets;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This function takes a JIRA issue key and retrieves the description from JIRA's API.
|
|
|
+ * It then searches the description for a GitHub closing link in the format "Closes https://github.com/owner/repo/issues/123".
|
|
|
+ * If a GitHub closing link is found, it is returned. If no GitHub closing link is found, it returns null.
|
|
|
+ * @param {string} jiraIssueKey - The key of the JIRA issue to search for the GitHub closing link.
|
|
|
+ * @returns {Promise<string|null>} - A promise that resolves to a string containing the GitHub closing link if found,
|
|
|
+ * or null if not found.
|
|
|
+ */
|
|
|
+ async function getGitHubClosingLink(jiraIssueKey) {
|
|
|
+ let jiraDescrition = "";
|
|
|
+
|
|
|
+ // Get JIRA ticket description content
|
|
|
+ try {
|
|
|
+ const response = await axios({
|
|
|
+ url: `https://jira.espressif.com:8443/rest/api/latest/issue/${jiraIssueKey}`,
|
|
|
+ auth: {
|
|
|
+ username: process.env.DANGER_JIRA_USER,
|
|
|
+ password: process.env.DANGER_JIRA_PASSWORD,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ jiraDescrition = response.data.fields.description;
|
|
|
+ } catch (error) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Find GitHub closing link in description
|
|
|
+ const regexClosingGhLink =
|
|
|
+ /Closes\s+(https:\/\/github.com\/\S+\/\S+\/issues\/\d+)/;
|
|
|
+ const closingGithubLink = jiraDescrition.match(regexClosingGhLink);
|
|
|
+
|
|
|
+ if (closingGithubLink) {
|
|
|
+ return closingGithubLink[1];
|
|
|
+ } else {
|
|
|
+ return false; // Jira issue has no GitHub closing link in description
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if a GitHub issue linked in a merge request is still open.
|
|
|
+ *
|
|
|
+ * @param {string} link - The link to the GitHub issue.
|
|
|
+ * @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the issue is open.
|
|
|
+ * @throws {Error} If the link is invalid or if there was an error fetching the issue.
|
|
|
+ */
|
|
|
+ async function isGithubIssueOpen(link) {
|
|
|
+ const parsedUrl = new URL(link);
|
|
|
+ const [owner, repo] = parsedUrl.pathname.split("/").slice(1, 3);
|
|
|
+ const issueNumber = parsedUrl.pathname.split("/").slice(-1)[0];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await axios.get(
|
|
|
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`
|
|
|
+ );
|
|
|
+ return response.data.state === "open"; // return True if GitHub issue is open
|
|
|
+ } catch (error) {
|
|
|
+ return null; // GET request to issue fails
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function createReport() {
|
|
|
+ partMessages.sort();
|
|
|
+ let dangerMessage = `Some issues found for the related JIRA tickets in this MR:\n${partMessages.join(
|
|
|
+ "\n"
|
|
|
+ )}`;
|
|
|
+ warn(dangerMessage);
|
|
|
+ }
|
|
|
+};
|