io:fwrite("JIRA vs Excel\n").


ENG ( Link to Portuguese versiao )

If you work in any of hiper-big, intergalactic enterprise that is in TOP of the richest companies there I'm almost sure that you have possibility to work with the oldest JIRA and Excel that is secure enough :-].
Colleague ask me if it possible to do Excel reporting from JIRA. As must have requirement: report must have information about issue comments.
Below you can find a quick, dirty, UNSECURED PoC. Be sure you use it only in your lab. Remember, that below there is Base 64 ENCODING (not encryption).

Lets take a few assumptions:
>you can use Excel 2016,
> information are stored on local JIRA Server,
> you can use Base64 ENCODING as authentication,
> you must work @ WIN environment.

We can start be preparing Base64 string that we will use in authentication. In powershell:
$user=michal
$password=CDkew8273%34wjs8
$pair="$($user):$($pass)"
$enc64=[System.Convert]::ToBase64String([System.Text.encoding]::ASCII.GetBytes($pair))

Now we can play with Excel. In Data -> New Query -> From Other Sources choose "Blank Query". For the start you should be able to create in advance editor:
let
   Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
in
  Source

After that you can play with the data without the pain. The write-only method use {row-number} and [column-name] to do the filtering.

Nicer way is to use dedicated features like: Table.Transpose, Table.SelectColumns, …


List of JIRA issues id you can get as:
let
   Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
   #"Convered to Table" = Record.ToTable(Source),
   #"Transposed Table" = Table.Transpose(#"Convered to Table"),
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   #Removed Other Columns" = Table.SelectColumns(#"Promoted Headers", {"issues"}),
   issues = #"Removed Other Columns"{0}[issues],
   #"Converted to Table1" = Table.FromList(issues, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   #"Expanded Column1" = Table.ExpandRecordColumn(#Converted to Table1", "Column1", {"key"}, {"Column1.id"})
in
   #"Expanded Column1"


The next step would be to create excel-like, click-oriented query where user can choose what he would like to observe (solution from: https://community.powerbi.com/t5/Desktop/JIRA-amp-PowerBI/m-p/161986):
let
   // The same query as before
   Source = Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]])),
   // Converting data from List to Table, so
   #"Converted to Table" = Record.ToTable(Source),
   // we will be able to transpose it
   #"Transposed Table" = Table.Transpose(#"Converted to Table"),
   // and make the field names the column headers
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   // Now we are going to assign the correct data types for each column
   #"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{{"expand", type text}, {"startAt", Int64.Type}, {"maxResults", Int64.Type}, {"total", Int64.Type}, {"issues", type any}}),
   // And keep only the total column
   #"Removed Other Columns" = Table.SelectColumns(#"Changed Type",{"total"}),
   // Now we are going to get the first (and only) record and assign it to a variable named #"total"
   #"total" = #"Removed Other Columns"{0}[total],
   // Now it is time to build a list of startAt values, starting on 0, incrementing 100 per item
   #"startAt List" = List.Generate(()=>0, each _ < #"total", each _ +100),
   // Converting the startAt list into Table - not sure if this is really necessary
   #"Converted to Table1" = Table.FromList(#"startAt List", Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   // and renaming its single column
   #"Renamed Columns" = Table.RenameColumns(#"Converted to Table1",{{"Column1", "startAt"}}),
   // Now let's create a new column URL with the URL we are interested at. Note the maxResults and startAt parameters this time
   #"Added Custom" = Table.AddColumn(#"Renamed Columns", "URL", each "http://jira.my.domain/rest/api/latest/search?&maxResults=100&jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED&startAt=" & Text.From([startAt])),
   // And finally retrieve them all
   data = List.Transform(#"Added Custom"[URL], each Json.Document(Web.Contents(_, [Headers=[Authorization="Basic value_of_enc64"]])))
in
   data


This should be the perfect moment for end but there is no comments in the query results.
After a moment with JIRA GUI you can find them in XML for each issue in: https://jira.my.domain/si/jira.issueviews:issue-xml/TEST-1021/TEST-1021.xml. The URL for each issue is so nice that we should be able to build it based on our initial list of issues:

let    Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
   #"Convered to Table" = Record.ToTable(Source),
   #"Transposed Table" = Table.Transpose(#"Convered to Table"),
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   #Removed Other Columns" = Table.SelectColumns(#"Promoted Headers", {"issues"}),
   issues = #"Removed Other Columns"{0}[issues],
   #"Converted to Table1" = Table.FromList(issues, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   #"Issue Key" = Table.ExpandRecordColumn(#Converted to Table1", "Column1", {"key"}, {"Column1.id"}),
   #"Insert Last Comment" = Table.AddColumn(#"Issue Key", "Last Comment",
each
   Table.Last(
     Table.SelectRows(
       Table.SelectRows(
         XML.Document(
           Web.Contents(
             Text.Combine({"https://jira.my.domain/si/jira.issueviews:issue-xml/", [Column1.id], "/", [Column1.id], ".xml"}),
             [Headers=[Authorization="Basic value_of_enc64"]]
           )
         ){0}[Value]{0}[Value], each [Name] = "item"){0}[Value],
       each [Name] = "comments"
     )[Value]{0}
   )[Value]
in
   #"Insert Last Comment"


If you will play with the code anywhere else than your lab - remember to change Base64 for OAuth or session cookie.

PT

Se trabalhas em alguma empresa hiper-grande e intergaláctica que está no TOP das empresas mais ricas de lá, tenho quase certeza de que tens a possibilidade de trabalhar com o JIRA e o Excel mais antigos que são seguros o suficiente :-].
Colega me pergunte se é possível fazer relatórios em Excel a partir do JIRA. Como deve ter o requisito: o relatório deve ter informações sobre os comentários do JIRA issue.
Abaixo, podes encontrar um PoC rápido, sujo e NÃO SEGURO. Certifique-se de usá-lo apenas em seu laboratório. Lembre-se que abaixo está a ENCODING em Base 64 (não encryption).

Vamos fazer algumas suposições: > podes usar o Excel 2016,
> as informações são armazenadas no servidor JIRA local,
> você pode usar a ENCODING Base64 como autenticação,
> deves trabalhar @ ambiente WIN..

Podemos começar preparando a string Base64 que usaremos na autenticação. No powershell::
$user=michal
$password=CDkew8273%34wjs8
$pair="$($user):$($pass)"
$enc64=[System.Convert]::ToBase64String([System.Text.encoding]::ASCII.GetBytes($pair))

Now we can play with Excel. In Data -> New Query -> From Other Sources choose "Blank Query". For the start you should be able to create in advance editor:
let
   Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
in
  Source

Depois disso, podes brincar com os dados sem dor. O método somente gravação usa {row-number} e [column-name] para fazer a filtragem.

A maneira mais agradável é usar recursos dedicados como: Table.Transpose, Table.SelectColumns, …


Lista de IDs de pendências do JIRA que podes obter como:
let
   Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
   #"Convered to Table" = Record.ToTable(Source),
   #"Transposed Table" = Table.Transpose(#"Convered to Table"),
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   #Removed Other Columns" = Table.SelectColumns(#"Promoted Headers", {"issues"}),
   issues = #"Removed Other Columns"{0}[issues],
   #"Converted to Table1" = Table.FromList(issues, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   #"Expanded Column1" = Table.ExpandRecordColumn(#Converted to Table1", "Column1", {"key"}, {"Column1.id"})
in
   #"Expanded Column1"


O próximo passo seria criar uma consulta orientada a cliques do tipo Excel, onde o usuário pode escolher o que gostaria de observar (solução de: https://community.powerbi.com/t5/Desktop/JIRA-amp-PowerBI/m-p/161986):
let
   // The same query as before
   Source = Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]])),
   // Converting data from List to Table, so
   #"Converted to Table" = Record.ToTable(Source),
   // we will be able to transpose it
   #"Transposed Table" = Table.Transpose(#"Converted to Table"),
   // and make the field names the column headers
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   // Now we are going to assign the correct data types for each column
   #"Changed Type" = Table.TransformColumnTypes(#"Promoted Headers",{{"expand", type text}, {"startAt", Int64.Type}, {"maxResults", Int64.Type}, {"total", Int64.Type}, {"issues", type any}}),
   // And keep only the total column
   #"Removed Other Columns" = Table.SelectColumns(#"Changed Type",{"total"}),
   // Now we are going to get the first (and only) record and assign it to a variable named #"total"
   #"total" = #"Removed Other Columns"{0}[total],
   // Now it is time to build a list of startAt values, starting on 0, incrementing 100 per item
   #"startAt List" = List.Generate(()=>0, each _ < #"total", each _ +100),
   // Converting the startAt list into Table - not sure if this is really necessary
   #"Converted to Table1" = Table.FromList(#"startAt List", Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   // and renaming its single column
   #"Renamed Columns" = Table.RenameColumns(#"Converted to Table1",{{"Column1", "startAt"}}),
   // Now let's create a new column URL with the URL we are interested at. Note the maxResults and startAt parameters this time
   #"Added Custom" = Table.AddColumn(#"Renamed Columns", "URL", each "http://jira.my.domain/rest/api/latest/search?&maxResults=100&jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED&startAt=" & Text.From([startAt])),
   // And finally retrieve them all
   data = List.Transform(#"Added Custom"[URL], each Json.Document(Web.Contents(_, [Headers=[Authorization="Basic value_of_enc64"]])))
in
   data


Este deve ser o momento perfeito para terminar, mas não há comentários nos resultados da consulta..
Após um momento com a GUI do JIRA, podes encontrá-los em XML para cada issue em: https://jira.my.domain/si/jira.issueviews:issue-xml/TEST-1021/TEST-1021.xml. O URL para cada problema é tão bom que devemos ser capazes de construí-lo com base em nossa lista inicial de issues:

let    Source = "Json.Document(Web.contents("http://jira.my.domain/rest/api/latest/search?jql=project+%3D+TEST+STATUS+NOT+IN+CLOSED", [Headers=[Authorization="Basic value_of_enc64"]]))"
   #"Convered to Table" = Record.ToTable(Source),
   #"Transposed Table" = Table.Transpose(#"Convered to Table"),
   #"Promoted Headers" = Table.PromoteHeaders(#"Transposed Table", [PromoteAllScalars=true]),
   #Removed Other Columns" = Table.SelectColumns(#"Promoted Headers", {"issues"}),
   issues = #"Removed Other Columns"{0}[issues],
   #"Converted to Table1" = Table.FromList(issues, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
   #"Issue Key" = Table.ExpandRecordColumn(#Converted to Table1", "Column1", {"key"}, {"Column1.id"}),
   #"Insert Last Comment" = Table.AddColumn(#"Issue Key", "Last Comment",
each
   Table.Last(
     Table.SelectRows(
       Table.SelectRows(
         XML.Document(
           Web.Contents(
             Text.Combine({"https://jira.my.domain/si/jira.issueviews:issue-xml/", [Column1.id], "/", [Column1.id], ".xml"}),
             [Headers=[Authorization="Basic value_of_enc64"]]
           )
         ){0}[Value]{0}[Value], each [Name] = "item"){0}[Value],
       each [Name] = "comments"
     )[Value]{0}
   )[Value]
in
   #"Insert Last Comment"


Se tu vais jogar com o código em qualquer outro lugar que não seja seu laboratório - lembre-se de alterar Base64 para OAuth ou cookie de sessão.

Last modification: 2022-07-24 Sun 21:10

By Ziółkowski Michał

License: Atribution-ShareAlike 4.0 International (CC BY-SA 4.0)