exportJiraSprintChangelogIssues

Jody Winter Ralf D. Müller Vladi

1 minute to read

About This Task

This task exports a simplified (key and summary) list of Jira issues for a specific sprint defined in the task configuration. Only a few additional fields (such as assignee) can be switched using configuration flags.

Once you define the sprint, the relevant AsciiDoc and Excel files will be generated. If a sprint is not defined in the configuration, changelogs for all sprints that match the configuration will be saved in separate AsciiDoc files and in different tabs within an Excel file.

The task configuration can be found within Config.gradle. In addition to the configuration snippet below, it is important to configure the Jira API and credentials in the Jira section of the configuration inside the same file.

Configuration

Config.groovy
// Sprint changelog configuration generate changelog lists based on tickets in sprints of an Jira instance.
// This feature requires at least Jira API & credentials to be properly set in Jira section of this configuration
sprintChangelog = [:]
sprintChangelog.with {
    sprintState = 'closed' // it is possible to define multiple states, i.e. 'closed, active, future'
    ticketStatus = "Done, Closed" // it is possible to define multiple ticket statuses, i.e. "Done, Closed, 'in Progress'"

    showAssignee = false
    showTicketStatus = false
    showTicketType = true
    sprintBoardId = 12345  // Jira instance probably have multiple boards; here it can be defined which board should be used

    // Output folder for this task inside main outputPath
    resultsFolder = 'Sprints'

    // if sprintName is not defined or sprint with that name isn't found, release notes will be created on for all sprints that match sprint state configuration
    sprintName = 'PRJ Sprint 1' // if sprint with a given sprintName is found, release notes will be created just for that sprint
    allSprintsFilename = 'Sprints_Changelogs' // Extension will be automatically added.
}

Source

exportJiraSprintChangelog.gradle
task exportJiraSprintChangelog(
        description: 'exports all jira issues from Sprint for release notes',
        group: 'docToolchain'
) {
    doLast {
        // Pre defined ticket fields for Changelog based on Jira Sprints
        def defaultTicketFields = 'summary,status,assignee,issuetype'

        // retrieving sprints for a given board
        def sprints = { apiSprints, headers, boardId, sprintState ->
                apiSprints.get(path: "agile/latest/board/${boardId}/sprint",
                    query:[state: "${sprintState}"],
                    headers: headers
                ).data
        }

        // retrieving issues for given sprint
        def issues = { apiIssues, headers, boardId, sprintId, status ->
            apiIssues.get(path: "agile/latest/board/${boardId}/sprint/${sprintId}/issue",
                    query: ['jql'       : "status in (${status}) ORDER BY type DESC, status ASC",
                            'maxResults': 1000,
                            fields: defaultTicketFields
                    ],
                    headers: headers
            ).data
        }

        // preparing target folder for generated files
        final String taskSubfolderName = config.sprintChangelog.resultsFolder
        final File targetFolder = new File(targetDir + File.separator + taskSubfolderName)
        if (!targetFolder.exists()) targetFolder.mkdirs()
        logger.debug("Output folder for 'exportJiraSprintChangelog' task is: '${targetFolder}'")

        // Getting configuration
        def jiraRoot = config.jira.api
        def jiraProject = config.jira.project

        def sprintState = config.sprintChangelog.sprintState
        def ticketStatusForReleaseNotes = config.sprintChangelog.ticketStatus
        def sprintBoardId = config.sprintChangelog.sprintBoardId
        def showAssignee = config.sprintChangelog.showAssignee
        def showTicketStatus = config.sprintChangelog.showTicketStatus
        def showTicketType = config.sprintChangelog.showTicketType
        def sprintName = config.sprintChangelog.sprintName
        def allSprintsFilename = config.sprintChangelog.allSprintsFilename

        logger.info("\n==========================\nJira Release notes config\n==========================")
        logger.info("Spring Board ID: ${sprintBoardId}")
        logger.info("Show assignees: ${showAssignee}. Show ticket status: ${showTicketStatus}. Show ticket type: ${showTicketType}")
        logger.info("Filtering for sprints with configured state: '${sprintState}'")
        logger.info("Filtering for issues with configured statuses: ${ticketStatusForReleaseNotes}")
        logger.info("Attempt to generate release notes for sprint with a name: '${sprintName}'")
        logger.info("Filename used for all sprints: '${allSprintsFilename}'")

        def api = new groovyx.net.http.RESTClient(jiraRoot + '/rest/')
        api.encoderRegistry = new groovyx.net.http.EncoderRegistry(charset: 'utf-8')
        def headers = [
                'Authorization': "Basic " + config.jira.credentials,
                'Content-Type' : 'application/json; charset=utf-8'
        ]

        def allChangelogsFilename = "${allSprintsFilename}.xlsx"
        logger.quiet("Changelogs of all sprints will be saved in '${allChangelogsFilename}' file")

        def changelogsXls = new File(targetFolder, allChangelogsFilename)
        def changelogsXlsFos = new FileOutputStream(changelogsXls)
        Workbook wb = new XSSFWorkbook();
        CreationHelper hyperlinkHelper = wb.getCreationHelper();

        String rgbS = "A7A7A7"
        byte[] rgbB = Hex.decodeHex(rgbS) // get byte array from hex string
        XSSFColor color = new XSSFColor(rgbB, null) //IndexedColorMap has no usage until now. So it can be set null.
        XSSFCellStyle headerCellStyle = (XSSFCellStyle) wb.createCellStyle()
        headerCellStyle.setFillForegroundColor(color)
        headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND)

        // prepare tickets according to configuration
        def columns = ['key'].plus(defaultTicketFields.split(',').collect())
        if (!showAssignee) { columns = columns.minus('assignee')}
        if (!showTicketStatus) { columns = columns.minus('status')}
        if (!showTicketType) { columns = columns.minus('issuetype')}
        logger.info("Release notes will contain following info: ${columns}")

        logger.info("\n=====================\n      Sprints\n=====================")
        //
        def allMatchedSprints = sprints(api, headers, sprintBoardId, sprintState).values
        def foundExactSprint = allMatchedSprints.any {it.name == sprintName}
        logger.info("All sprints that matched configuration: ${allMatchedSprints.size()}")

        def sprintsForChangelog = foundExactSprint ? allMatchedSprints.stream().filter() {it.name == sprintName} : allMatchedSprints
        logger.info("Found exact Sprint with name '${sprintName}': ${foundExactSprint}.")

        sprintsForChangelog.each { sprint ->
            logger.quiet("\nSprint: $sprint.name [id: $sprint.id] state <$sprint.state>")

            /* ================================================
                Create new worksheet inside existing excel file
               ================================================ */
            String safeSprintName = WorkbookUtil.createSafeSheetName("${sprint.name}")
            def ws = wb.createSheet(safeSprintName)

            // Add titles (typically key & summary, but assignee, ticket status, ticket type can be configured in Config.groovy too)
            def titleRow = ws.createRow(0);
            int cellNumber = 0;
            columns.each {columnTitle -> titleRow.createCell(cellNumber++).setCellValue("${columnTitle.capitalize()}")}

            def lastRow = titleRow.getRowNum()
            titleRow.setRowStyle(headerCellStyle)
            // set summary (at position 1) column wider than other columns
            ws.setColumnWidth(1, 35*256)

            /* =========================================
                      AsciiDoc file for each sprint
               ========================================= */
            def asciidocFilename = "${sprint.name.replaceAll(" ", "_")}.adoc"
            logger.info("Results will be saved in '${asciidocFilename}' file")

            def changeLogAdoc = new File(targetFolder, "${asciidocFilename}")
            changeLogAdoc.write(".Table ${sprint.name} Changelog\n", 'utf-8')
            changeLogAdoc.append("|=== \n")

            // AsciiDoc table columns
            columns.each {columnTitle -> changeLogAdoc.append("|${columnTitle} ", 'utf-8')}

            /* =========================================
                      Add tickets for the sprint
               ========================================= */
            issues(api, headers, sprintBoardId, sprint.id, ticketStatusForReleaseNotes).issues.each {issue ->
                def assignee = "${issue.fields.assignee ? issue.fields.assignee.displayName : 'unassigned'} "
                def message = showAssignee ? "by ${assignee} ": ""
                logger.quiet("Issue: [$issue.key] '$issue.fields.summary' ${message}<$issue.fields.status.name>")

                /* ===========================
                      Write ticket to Excel
                   =========================== */
                int cellPosition = 0
                def row = ws.createRow(++lastRow)

                Hyperlink link = hyperlinkHelper.createHyperlink(HyperlinkType.URL)
                link.setAddress("${jiraRoot}/browse/${issue.key}")
                Cell cellWithUrl = row.createCell(cellPosition)
                cellWithUrl.setCellValue("${issue.key}")
                cellWithUrl.setHyperlink(link)

                row.createCell(++cellPosition).setCellValue("${issue.fields.summary}")

                /* =============================
                      Write ticket to Asciidoc
                   ============================= */
                changeLogAdoc.append("\n", 'utf-8')
                changeLogAdoc.append("| ${jiraRoot}/browse/${issue.key}[${issue.key}] ", 'utf-8')
                changeLogAdoc.append("| ${issue.fields.summary} ", 'utf-8')

                /* === Write ticket status, assignee, ticket typee if configured to both Asciidoc & Excel files === */
                if (showTicketStatus) {
                    row.createCell(++cellPosition).setCellValue("${issue.fields.status.name}")
                    changeLogAdoc.append("| ${issue.fields.status.name} ", 'utf-8')
                }

                if (showAssignee) {
                    row.createCell(++cellPosition).setCellValue("${assignee}")
                    changeLogAdoc.append("| ${assignee}", 'utf-8')
                }

                if (showTicketType) {
                    row.createCell(++cellPosition).setCellValue("${issue.fields.issuetype.name}")
                    changeLogAdoc.append("| ${issue.fields.issuetype.name} ", 'utf-8')
                }
            }
            // Close the asciidoc table
            changeLogAdoc.append("\n|=== \n",'utf-8')

            // Set auto-width to KEY column
            ws.autoSizeColumn(0);
        }
        // Write to Excel file
        wb.write(changelogsXlsFos)
    }
}