Initial commit: tool to help analyze data from JIRA.
This commit is contained in:
commit
dfd80ccf6a
26
Makefile
Normal file
26
Makefile
Normal file
@ -0,0 +1,26 @@
|
||||
PGSQL_CONTAINER_ID=`cat postgres.container.id`
|
||||
|
||||
createdb:
|
||||
docker run \
|
||||
--name postgres-tegra118 \
|
||||
-e POSTGRES_PASSWORD=password \
|
||||
-p 5500:5432 \
|
||||
-d postgres \
|
||||
> postgres.container.id
|
||||
sleep 5
|
||||
PGPASSWORD=password psql -p 5500 -U postgres -h localhost \
|
||||
-c 'CREATE DATABASE tegra118;'
|
||||
|
||||
startdb:
|
||||
docker start $(PGSQL_CONTAINER_ID)
|
||||
|
||||
stopdb:
|
||||
docker stop $(PGSQL_CONTAINER_ID)
|
||||
|
||||
deletedb:
|
||||
-docker stop $(PGSQL_CONTAINER_ID)
|
||||
docker rm $(PGSQL_CONTAINER_ID)
|
||||
rm postgres.container.id
|
||||
|
||||
connect:
|
||||
PGPASSWORD=password psql -p 5500 -U postgres -h localhost tegra118
|
93
src/nim/tm_pm.nim
Normal file
93
src/nim/tm_pm.nim
Normal file
@ -0,0 +1,93 @@
|
||||
import csvtools, docopt, fiber_orm, db_postgres, sequtils, sets, strutils
|
||||
|
||||
import ./tm_pmpkg/jira_api
|
||||
|
||||
type
|
||||
Feature* = object
|
||||
id*: int
|
||||
name*: string
|
||||
epic*: int
|
||||
stories*: seq[string]
|
||||
defects*: seq[string]
|
||||
status*: string
|
||||
confidence*: int
|
||||
target_release*: string
|
||||
|
||||
TmPmDb* = ref object
|
||||
conn: DbConn
|
||||
|
||||
func connect(connString: string): TmPmDb =
|
||||
result = TmPmDb(conn: open("", "", "", connString))
|
||||
|
||||
generateProcsForModels(TmPmDb, [ChangeLog, Feature, Issue])
|
||||
|
||||
generateLookup(TmPmDb, ChangeLog, @["historyId"])
|
||||
|
||||
when isMainModule:
|
||||
|
||||
let doc = """
|
||||
Usage:
|
||||
tm_pm import-csv <import-file>
|
||||
tm_pm api-sync <username> <api-key>
|
||||
"""
|
||||
|
||||
let args = docopt(doc, version = "0.1.0")
|
||||
let db = connect("host=localhost port=5500 dbname=tegra118 user=postgres password=password")
|
||||
|
||||
if args["import-csv"]:
|
||||
let rows = toSeq(csvRows(path = $args["<import-file>"]))
|
||||
let jiraIssues = rows.map(proc (r: seq[string]): Issue =
|
||||
Issue(
|
||||
issueType: r[0],
|
||||
id: r[1],
|
||||
summary: r[2],
|
||||
priority: r[3],
|
||||
status: r[4],
|
||||
epicId: r[5],
|
||||
testPhase: r[6],
|
||||
assignee: r[7],
|
||||
linkedIssueIds: r[8..<r.len].filterIt(not it.isEmptyOrWhitespace)
|
||||
))
|
||||
|
||||
for issue in jiraIssues:
|
||||
discard db.createIssue(issue);
|
||||
# see if the issue already exists
|
||||
# try:
|
||||
# let existingRecord = db.getJiraIssue(issue.id);
|
||||
# except NotFoundError:
|
||||
# db.createJiraIssue(issue);
|
||||
|
||||
if args["api-sync"]:
|
||||
initJiraClient("https://tegra118.atlassian.net", $args["<username>"], $args["<api-key>"])
|
||||
let issuesAndChangelogs = searchIssues(
|
||||
"project = \"UUP\" and (labels is empty or labels != \"Design&Reqs\") ORDER BY key ASC",
|
||||
includeChangelog = true
|
||||
)
|
||||
|
||||
var issuesUpdated = 0
|
||||
var issuesCreated = 0
|
||||
var changelogsCreated = 0
|
||||
|
||||
stdout.write("\nRetrieved " & $issuesAndChangelogs[0].len & " issues. ")
|
||||
for issue in issuesAndChangelogs[0]:
|
||||
try:
|
||||
discard db.getIssue(issue.id)
|
||||
discard db.updateIssue(issue)
|
||||
issuesUpdated += 1;
|
||||
except NotFoundError:
|
||||
discard db.createIssue(issue)
|
||||
issuesCreated += 1;
|
||||
stdout.writeLine("Created " & $issuesCreated & " and updated " & $issuesUpdated)
|
||||
|
||||
stdout.write("Retrieved " & $issuesAndChangelogs[1].len & " change logs. ")
|
||||
var newHistoryIds: HashSet[string] = initHashSet[string]()
|
||||
for changelog in issuesAndChangelogs[1]:
|
||||
try:
|
||||
if newHistoryIds.contains(changelog.historyId) or
|
||||
db.findChangeLogsByHistoryId(changelog.historyId).len == 0:
|
||||
newHistoryIds.incl(changelog.historyId)
|
||||
discard db.createChangeLog(changelog)
|
||||
changelogsCreated += 1;
|
||||
except NotFoundError: discard
|
||||
|
||||
stdout.writeLine("Recorded " & $changelogsCreated & " we didn't already have.\n")
|
1
src/nim/tm_pm.nim.cfg
Normal file
1
src/nim/tm_pm.nim.cfg
Normal file
@ -0,0 +1 @@
|
||||
--d:ssl
|
111
src/nim/tm_pmpkg/jira_api.nim
Normal file
111
src/nim/tm_pmpkg/jira_api.nim
Normal file
@ -0,0 +1,111 @@
|
||||
import base64, httpclient, json, sequtils, strutils, times, uri
|
||||
|
||||
type
|
||||
ChangeLog* = object
|
||||
id*: string
|
||||
historyId*: string
|
||||
issueId*: string
|
||||
author*: string
|
||||
createdAt*: DateTime
|
||||
field*: string
|
||||
oldValue*: string
|
||||
newValue*: string
|
||||
|
||||
Issue* = object
|
||||
id*: string
|
||||
issueType*: string
|
||||
summary*: string
|
||||
epicId*: string
|
||||
assignee*: string
|
||||
status*: string
|
||||
priority*: string
|
||||
linkedIssueIds*: seq[string]
|
||||
testPhase*: string
|
||||
|
||||
let client = newHttpClient()
|
||||
var API_BASE = "";
|
||||
const FIELDS = "issuetype,summary,customfield_10014,assignee,status,priority,issuelinks,customfield_10218,changelog"
|
||||
|
||||
proc parseIssue(json: JsonNode): (Issue, seq[ChangeLog]) =
|
||||
let f = json["fields"]
|
||||
return (
|
||||
Issue(
|
||||
id: json["key"].getStr(),
|
||||
issueType: f["issuetype"]["name"].getStr(),
|
||||
summary: f["summary"].getStr(),
|
||||
epicId: f["customfield_10014"].getStr(),
|
||||
assignee:
|
||||
if f["assignee"].kind == JNull: "Unassigned"
|
||||
else: f["assignee"]["displayName"].getStr(),
|
||||
status: f["status"]["name"].getStr(),
|
||||
priority: f["priority"].getStr(),
|
||||
linkedIssueIds: f["issuelinks"].mapIt(
|
||||
if it.hasKey("inwardIssue"): it["inwardIssue"]["key"].getStr()
|
||||
else: it["outwardIssue"]["key"].getStr()),
|
||||
testPhase: f["customfield_10218"].getStr()),
|
||||
if json.hasKey("changelog") and json["changelog"]["histories"].getElems().len > 0:
|
||||
json["changelog"]["histories"].getElems().map(
|
||||
proc (h: JsonNode): seq[ChangeLog] = h["items"].mapIt(
|
||||
ChangeLog(
|
||||
historyId: h["id"].getStr(),
|
||||
issueId: json["key"].getStr(),
|
||||
author: h["author"]["displayName"].getStr(),
|
||||
createdAt: parse(
|
||||
h["created"].getStr()[0..17] & h["created"].getStr()[^6..^3],
|
||||
"yyyy-MM-dd'T'HH:mm:sszz"),
|
||||
field: it["field"].getStr(),
|
||||
oldValue: it["fromString"].getStr(),
|
||||
newValue: it["toString"].getStr()
|
||||
)
|
||||
)
|
||||
).foldl(a & b)
|
||||
else: @[]
|
||||
)
|
||||
|
||||
proc initJiraClient*(apiBasePath: string, username: string, apiToken: string) =
|
||||
API_BASE = apiBasePath
|
||||
client.headers = newHttpHeaders({
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Basic " & encode(username & ":" & apiToken)
|
||||
})
|
||||
|
||||
proc searchIssues*(jql: string, includeChangelog: bool = false):
|
||||
(seq[Issue], seq[ChangeLog]) =
|
||||
|
||||
result = (@[], @[])
|
||||
|
||||
var query = @[
|
||||
("jql", jql),
|
||||
("fields", FIELDS)
|
||||
]
|
||||
|
||||
if includeChangelog: query.add(("expand", "changelog"))
|
||||
|
||||
var resp = client.get(API_BASE & "/rest/api/3/search?" & encodeQuery(query))
|
||||
|
||||
while true:
|
||||
if not resp.status.startsWith("2"):
|
||||
raise newException(Exception,
|
||||
"Received error from API: " & resp.status &
|
||||
"\nHeaders: " & $resp.headers &
|
||||
"\nBody: " & $resp.body)
|
||||
|
||||
let body = parseJson(resp.body)
|
||||
let nextStartAt = body["startAt"].getInt(0) + body["maxResults"].getInt(0)
|
||||
|
||||
echo "Retrieved records " &
|
||||
$body["startAt"].getInt() & " to " &
|
||||
$(nextStartAt - 1) & " of " &
|
||||
$body["total"].getInt() &
|
||||
" (" & $body["issues"].getElems().len & " records received)"
|
||||
|
||||
let issuesAndLogs = body["issues"].getElems().mapIt(parseIssue(it))
|
||||
|
||||
result[0] &= issuesAndLogs.mapIt(it[0])
|
||||
result[1] &= issuesAndLogs.mapIt(it[1]).foldl(a & b)
|
||||
|
||||
if nextStartAt > body["total"].getInt(): break
|
||||
|
||||
resp = client.get(
|
||||
API_BASE & "/rest/api/3/search?" &
|
||||
encodeQuery(query & ("startAt", $nextStartAt)))
|
34
src/sql/01-schema-up.sql
Normal file
34
src/sql/01-schema-up.sql
Normal file
@ -0,0 +1,34 @@
|
||||
CREATE TABLE issues (
|
||||
id varchar primary key,
|
||||
issue_type varchar not null,
|
||||
summary varchar not null,
|
||||
epicId varchar,
|
||||
assignee varchar,
|
||||
test_phase varchar,
|
||||
status varchar not null,
|
||||
priority varchar not null,
|
||||
linked_issue_ids varchar[]
|
||||
);
|
||||
|
||||
CREATE TABLE features (
|
||||
id serial primary key,
|
||||
name varchar not null,
|
||||
epicId varchar not null default '',
|
||||
stories varchar[] not null default '{}',
|
||||
defects varchar[] not null default '{}',
|
||||
status varchar default 'todo',
|
||||
confidence int not null default 0,
|
||||
target_release varchar not null default '',
|
||||
notes varchar not null default ''
|
||||
);
|
||||
|
||||
CREATE TABLE change_logs (
|
||||
id serial primary key,
|
||||
history_id varchar,
|
||||
issue_id varchar not null references issues(id),
|
||||
author varchar,
|
||||
created_at timestamp with time zone,
|
||||
field varchar not null,
|
||||
old_value varchar,
|
||||
new_value varchar
|
||||
);
|
7
src/sql/02-bidirectional-story-links.sql
Normal file
7
src/sql/02-bidirectional-story-links.sql
Normal file
@ -0,0 +1,7 @@
|
||||
UPDATE jira_issues SET linked_issues = collected.linked_issues from (
|
||||
SELECT a.id, array_remove(array_cat(a.linked_issues, array_agg(b.id)) as linked_issues, NULL) FROM
|
||||
jira_issues a LEFT OUTER JOIN
|
||||
jira_issues b ON b.linked_issues @> ARRAY[a.id]
|
||||
GROUP BY a.id
|
||||
) AS collected
|
||||
WHERE jira_issues.id = collected.id;
|
44
src/sql/queries.sql
Normal file
44
src/sql/queries.sql
Normal file
@ -0,0 +1,44 @@
|
||||
-- Show bugs moved to 'Resolved' with a full accounting of everyone who has
|
||||
-- touched the issue, most recent issues first.
|
||||
SELECT
|
||||
i.id,
|
||||
i.epic_id,
|
||||
i.status,
|
||||
i.test_phase,
|
||||
-- i.summary,
|
||||
i.assignee,
|
||||
array_agg(DISTINCT c2.author) AS involved,
|
||||
c.created_at AS resolved_at
|
||||
FROM
|
||||
issues i JOIN
|
||||
change_logs c ON
|
||||
i.issue_type = 'Bug' AND
|
||||
i.id = c.issue_id AND
|
||||
c.field = 'status' AND
|
||||
c.new_value = 'Resolved' JOIN
|
||||
change_logs c2 on i.id = c2.issue_id
|
||||
GROUP BY
|
||||
i.id,
|
||||
i.epic_id,
|
||||
i.status,
|
||||
i.test_phase,
|
||||
-- i.summary,
|
||||
i.assignee,
|
||||
resolved_at
|
||||
ORDER BY resolved_at DESC;
|
||||
|
||||
-- Show everyone involved with a specific ticket
|
||||
SELECT
|
||||
i.id,
|
||||
i.epic_id,
|
||||
i.status,
|
||||
i.summary,
|
||||
array_agg(DISTINCT c.author) AS involved
|
||||
FROM
|
||||
issues i JOIN
|
||||
change_logs c ON i.id = c.issue_id
|
||||
WHERE i.id in ('UUP-848')
|
||||
GROUP BY i.id, i.epic_id, i.status;
|
||||
|
||||
|
||||
select status, count(*) from issues where issue_type = 'Bug' group by status;
|
15
tm_pm.nimble
Normal file
15
tm_pm.nimble
Normal file
@ -0,0 +1,15 @@
|
||||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
author = "Jonathan Bernard"
|
||||
description = "A new awesome nimble package"
|
||||
license = "MIT"
|
||||
srcDir = "src/nim"
|
||||
bin = @["tm_pm"]
|
||||
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires @["nim >= 1.4.0", "docopt", "uuids", "timeutils", "fiber_orm >= 0.3.1"]
|
||||
#requires "https://git.jdb-software.com/jdb-software/fiber-orm-nim.git"
|
||||
requires "https://github.com/andreaferretti/csvtools.git"
|
Loading…
Reference in New Issue
Block a user