2023-02-21 02:56:09 -05:00
/ * * C h e c k t h a t t h e r e a r e v a l i d J I R A l i n k s i n M R d e s c t i p t i o n .
*
* 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
) ;
2023-03-08 09:29:08 -05:00
const jiraTicketRegex = /[A-Z0-9]+-[0-9]+/ ;
2023-02-21 02:56:09 -05:00
let partMessages = [ ] ; // Create a blank field for future records of individual issues
// Parse section "Related" from MR Description
const sectionRelated = extractSectionRelated ( mrDescription ) ;
if (
2023-03-08 09:29:08 -05:00
! sectionRelated . header || // No section Related in MR description or ...
! jiraTicketRegex . test ( sectionRelated . content ) // no Jira links in section Related
2023-02-21 02:56:09 -05:00
) {
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 (
2023-03-08 09:29:08 -05:00
` - closing ticket \` ${ ticket . record } \` seems to be in the wrong format (or inaccessible to Jira DangerBot).. The correct format is for example \` - Closes JIRA-123 \` . `
2023-02-21 02:56:09 -05:00
) ;
}
// 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
}
2023-03-08 09:29:08 -05:00
const correctJiraClosingLinkFormat = new RegExp (
` ^- Closes ${ jiraTicketRegex . source } $ `
) ;
2023-02-21 02:56:09 -05:00
if ( ! correctJiraClosingLinkFormat . test ( line ) ) {
closingTickets . push ( {
record : line ,
2023-03-08 09:29:08 -05:00
ticketName : line . match ( jiraTicketRegex ) [ 0 ] ,
2023-02-21 02:56:09 -05:00
correctFormat : false ,
} ) ;
} else {
closingTickets . push ( {
record : line ,
2023-03-08 09:29:08 -05:00
ticketName : line . match ( jiraTicketRegex ) [ 0 ] ,
2023-02-21 02:56:09 -05:00
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 ) ;
}
} ;