Skip to content

Commit da9d518

Browse files
committed
Initial commit
0 parents  commit da9d518

File tree

2 files changed

+393
-0
lines changed

2 files changed

+393
-0
lines changed

README.org

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
* jira2org-story.py - Extract Jira Issues and Generate Org-mode Headings
2+
3+
This Python script fetches information of Jira issues and
4+
returns an Org-mode representation of those issues in Org-mode format.
5+
This Org-mode format is highly specific for my own purpose but you are
6+
able to modify this structure to meet your requirements. Therefore,
7+
this script works as a blueprint or template for you.
8+
9+
You can use one to many Jira issue numbers (for stories) as command
10+
line parameter. If you use Jira issues that are Epics, all
11+
corresponding stories are retrieved as well.
12+
13+
- Author: Karl Voit
14+
- License: GPLv3 or higher
15+
- Coding was started in May 2016
16+
17+
** Installation
18+
19+
1. Install Python version 2.x
20+
2. Install Python Jira library via ~pip install jira~ or similar
21+
3. Invoke the script via ~python ./jira2org-story.py~
22+
4. Generate the file ~jiraconfig.py~
23+
- similar to the template from the previous script output
24+
- in the same directory as the other Python script
25+
5. Re-start the script via ~python ./jira2org-story.py 1234~ and it
26+
should give you a similar output as stated below
27+
28+
** Command Line Parameters
29+
30+
#+BEGIN_SRC sh :results output :wrap quote
31+
python ./jira2org-story.py --help
32+
#+END_SRC
33+
34+
#+RESULTS:
35+
#+BEGIN_quote
36+
usage: jira2org-story.py [-h] [--version] IPD [IPD ...]
37+
38+
This tool retrieves a Jira issue and returns an Org-mode
39+
representation according to the system of Karl Voit.
40+
41+
The output is highly specific for my personal usage. If you want to have
42+
a similar functionality, you have to adapt it to your needs. This would
43+
require at least a search&replace of "IPD" with the Jira project ID of
44+
your choice, all Jira URLs, and the custom org-mode link "ipd:1234".
45+
46+
positional arguments:
47+
IPD One or many IPD numbers of stories or epics (without "IPD-"
48+
prefix)
49+
50+
optional arguments:
51+
-h, --help show this help message and exit
52+
--version show program's version number and exit
53+
54+
autor: Karl Voit <tools@Karl-Voit.at>
55+
license: GPL v3 or any later version
56+
URL: https://github.com/novoid/jira2org-story.py/
57+
bugreports: via GitHub
58+
version: Time-stamp: <2016-05-13 14:00:47 karl.voit>
59+
#+END_quote
60+
61+
** Example Output
62+
63+
As of [2016-04-24 Sun]:
64+
65+
#+BEGIN_SRC sh :results output :wrap quote
66+
python ./jira2org-story.py
67+
#+END_SRC
68+
69+
FIXXME

jira2org-story.py

+324
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
PROG_VERSION = u"Time-stamp: <2016-05-13 14:00:47 karl.voit>"
4+
5+
import sys
6+
import os
7+
import re
8+
import argparse
9+
import time
10+
11+
CONFIGDIR = os.getcwdu()
12+
CONFIGFILEBASENAME = 'jiraconfig'
13+
CONFIGFILENAME = os.path.join(CONFIGDIR, CONFIGFILEBASENAME)
14+
CONFIGTEMPLATE = '''
15+
JIRA_USER = 'joe'
16+
JIRA_PASSWORD = 'secret password for me'
17+
'''
18+
19+
DESCRIPTION = u'''This tool retrieves a Jira issue and returns an Org-mode
20+
representation according to the system of Karl Voit.
21+
22+
The output is highly specific for my personal usage. If you want to have
23+
a similar functionality, you have to adapt it to your needs. This would
24+
require at least a search&replace of "IPD" with the Jira project ID of
25+
your choice, all Jira URLs, and the custom org-mode link "ipd:1234".
26+
'''
27+
28+
EPILOG=u'''autor: Karl Voit <tools@Karl-Voit.at>
29+
license: GPL v3 or any later version
30+
URL: https://github.com/novoid/FIXXME/
31+
bugreports: via GitHub
32+
version: ''' + PROG_VERSION + '\n'
33+
34+
parser = argparse.ArgumentParser(description=DESCRIPTION,
35+
epilog=EPILOG,
36+
formatter_class=argparse.RawDescriptionHelpFormatter)
37+
38+
parser.add_argument('IPD', nargs='+', help='One or many IPD numbers of stories or epics (without \"IPD-\" prefix)')
39+
40+
verbosity_group = parser.add_mutually_exclusive_group()
41+
##verbosity_group.add_argument("--verbose", action="store_true")
42+
##verbosity_group.add_argument("--quiet", action="store_true")
43+
verbosity_group.add_argument('--version', action='version', version=PROG_VERSION)
44+
45+
args = parser.parse_args()
46+
47+
output_text = u'' # global output string for all stdout/file output operations
48+
49+
def print_line(text):
50+
global output_text
51+
output_text += text + '\n'
52+
print text
53+
54+
def print_item(ipd, message, level=1, empty_checkbox=False, filled_checkbox=False, omit_print=False):
55+
currentprefix = OUTPUTPREFIX + (level - 1) * u' '
56+
text = currentprefix + '- '
57+
if empty_checkbox:
58+
text += '[ ] '
59+
elif filled_checkbox:
60+
#text += '[X] '
61+
text += ''
62+
if ipd:
63+
text += orglink(ipd) + ' '
64+
text += message
65+
if omit_print:
66+
return text
67+
else:
68+
print_line(text)
69+
70+
71+
def thing2string(data, addlink=False):
72+
"simplify a set (or list) like (['foo', 'bar']) to 'foo, bar'"
73+
74+
if len(data) < 1:
75+
return ''
76+
77+
result = ''
78+
for item in data:
79+
if addlink:
80+
result += orglink(item) + ' '
81+
else:
82+
result += item + ', '
83+
if addlink:
84+
return result[:-1]
85+
else:
86+
return result[:-2]
87+
88+
89+
def orglink(text):
90+
"Takes a text like 'IPD-1234' and generates an Org-mode link"
91+
92+
return '[[' + text.replace('IPD-', 'https://product.infonova.at/jira/browse/IPD-') + '][' + text.replace('IPD-', '') + ']]'
93+
94+
95+
def get(issues, ipd, query):
96+
97+
return issues[ipd][query]
98+
99+
def extract_issue_fields(issue):
100+
101+
if issue.fields.customfield_10607:
102+
team = issue.fields.customfield_10607.value.encode('latin-1', 'ignore')
103+
else:
104+
team = None
105+
106+
if issue.fields.assignee:
107+
assignee = issue.fields.assignee.name.encode('latin-1', 'ignore')
108+
assigneelong = issue.fields.assignee.displayName.encode('latin-1', 'ignore')
109+
else:
110+
assignee = None
111+
assigneelong = None
112+
113+
if not issue.fields.customfield_11800:
114+
ghsprint = None
115+
else:
116+
ghsprint = re.sub(r'.+name=(.+?),.+$', r'\1', issue.fields.customfield_11800[0]),
117+
118+
return {'key': issue.key[4:],
119+
'fix_versions': [x.name for x in issue.fields.fixVersions],
120+
'created': issue.fields.created,
121+
'description': issue.fields.description,
122+
'issuetype': issue.fields.issuetype.name,
123+
'labels': issue.fields.labels,
124+
'assigneelong': assigneelong,
125+
'assignee': assignee,
126+
'reporter': issue.fields.reporter.displayName,
127+
'resolution': issue.fields.resolution,
128+
'resolutiondate': issue.fields.resolutiondate,
129+
'status': issue.fields.status.name,
130+
'summary': issue.fields.summary.encode('latin-1', 'ignore'),
131+
'ghsprint': ghsprint,
132+
'team': team,
133+
'priority': issue.fields.priority.name,
134+
'affects_versions': [x.name for x in issue.fields.versions]
135+
}
136+
137+
138+
139+
def retrieve_data_from_jira(ipds):
140+
141+
try:
142+
sys.path.insert(0, CONFIGDIR) # add cwd to Python path in order to find config file
143+
import jiraconfig # here, I was not able to use the CONFIGFILENAME variable
144+
except ImportError:
145+
print "\nERROR: Could not find \"" + CONFIGFILENAME + \
146+
"\".\nPlease generate such a file in the " + \
147+
"same directory as this script with following content and configure accordingly:\n" + \
148+
CONFIGTEMPLATE
149+
sys.exit(11)
150+
151+
try:
152+
from jira import JIRA
153+
except ImportError:
154+
print_line("ERROR: Could not find Python module \"JIRA\".\nPlease install it, e.g., with \"sudo pip install jira\".")
155+
sys.exit(12)
156+
157+
jira = JIRA('https://product.infonova.at/jira/', basic_auth=(jiraconfig.JIRA_USER, jiraconfig.JIRA_PASSWORD))
158+
159+
query = 'key = "IPD-' + '" or key = "IPD-'.join(ipds) + '" ORDER BY key'
160+
queryissues = jira.search_issues(query)
161+
162+
issues = []
163+
for issue in queryissues:
164+
165+
if issue.fields.issuetype.name == u'Epic':
166+
## if it is an Epic, query for its stories instead
167+
epicquery = 'project = ipd and type = Story and "Epic Link" = ' + issue.key + ' ORDER BY key'
168+
#print 'DEBUG: found epic, query=[' + epicquery + ']'
169+
epicissues = jira.search_issues(epicquery)
170+
issues.extend(retrieve_data_from_jira([x.key.replace('IPD-', '') for x in epicissues]))
171+
172+
elif issue.fields.issuetype.name == u'Story':
173+
#print 'DEBUG: found story'
174+
issues.append(extract_issue_fields(issue))
175+
176+
else:
177+
## report Defects and so forth
178+
print_line(u'ERROR: IPD-' + issue.key + ' is a ' + issue.fields.issuetype.name + \
179+
'. Only Stories and Epics are handled here.')
180+
sys.exit(13)
181+
182+
return issues
183+
184+
def print_issue(issue):
185+
186+
orgdate = time.strftime("%Y-%m-%d", time.localtime())
187+
orgtime = time.strftime("%Y-%m-%d %H:%M", time.localtime())
188+
DEFAULTSHORT = 'i' + issue['key']
189+
short = DEFAULTSHORT
190+
191+
print_line(u'')
192+
#print_item(None, u'Query time: ' + query_time)
193+
print_line(u'''
194+
** TODO [[IPD:''' + issue['''key'''] + ''']] ''' + issue['''summary'''] + \
195+
''' [0/7] :US_''' + short + ''':
196+
:PROPERTIES:
197+
:CREATED: [''' + orgtime + ''']
198+
:ID: ''' + orgdate + '''-Story-''' + short)
199+
print_line(u''':END:
200+
201+
| *IPD* | *Confluence* | *Champ* |''')
202+
203+
if issue['''assignee''']:
204+
champ = issue['''assignee''']
205+
else:
206+
champ = '''-'''
207+
208+
print_line(u''' | [[IPD:''' + issue['''key'''] + '''][''' + issue['''key'''] + ''']] | ''' + \
209+
issue['''summary'''] + ''' | ''' + champ + ''' |
210+
211+
*** STARTED create Jira [[IPD:%s]]''' % issue['''key'''])
212+
print_line(u''':PROPERTIES:
213+
:CREATED: [''' + orgtime + ''']
214+
:ID: ''' + orgdate + '''-''' + short + '''-create-jira-ipd
215+
:BLOCKER:
216+
:TRIGGER: ''' + orgdate + '''-''' + short + '''-define-champ(NEXT) ''' + \
217+
orgdate + '''-''' + short + '''-estimation(NEXT)
218+
:END:
219+
220+
- fill out:
221+
- [X] set reporter
222+
- [ ] set level red
223+
- [ ] fixVersion
224+
225+
*** NEXT create Confluence page with template
226+
SCHEDULED: <''' + orgdate + '''>
227+
:PROPERTIES:
228+
:CREATED: [''' + orgtime + ''']
229+
:ID: ''' + orgdate + '''-''' + short + '''-create-confluence-page
230+
:BLOCKER:
231+
:TRIGGER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria(NEXT)
232+
:END:
233+
234+
- fill out:
235+
- [ ] add Jira-Link [[IPD:''' + issue['''key'''] + ''']]
236+
- [ ] PO
237+
- [ ] Title
238+
- [ ] Business Value
239+
- [ ] add Confluence-short-URL to story table above
240+
241+
*** TODO write Acceptance Criteria, Docu, Perms
242+
:PROPERTIES:
243+
:CREATED: [''' + orgtime + ''']
244+
:ID: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria
245+
:BLOCKER: ''' + orgdate + '''-''' + short + '''-create-confluence-page
246+
:TRIGGER: ''' + orgdate + '''-''' + short + '''-confidence-green(NEXT) ''' + \
247+
orgdate + '''-''' + short + '''-hand-over-team(NEXT)
248+
:END:
249+
250+
*** TODO add Champ to Confluence and Jira :refinement:
251+
:PROPERTIES:
252+
:CATEGORY: refinement
253+
:CREATED: [''' + orgtime + ''']
254+
:ID: ''' + orgdate + '''-''' + short + '''-define-champ
255+
:BLOCKER:
256+
:END:
257+
258+
*** TODO get Estimation on [[IPD:''' + issue['''key'''] + \
259+
''']] :refinement:
260+
:PROPERTIES:
261+
:CREATED: [''' + orgtime + ''']
262+
:CATEGORY: refinement
263+
:ID: ''' + orgdate + '''-''' + short + '''-estimation
264+
:BLOCKER: ''' + orgdate + '''-''' + short + '''-create-jira-ipd
265+
:TRIGGER:
266+
:END:
267+
268+
- Estimation:
269+
270+
*** TODO get confidence-level green on [[IPD:''' + issue['''key'''] + \
271+
''']] :refinement:
272+
:PROPERTIES:
273+
:CATEGORY: refinement
274+
:CREATED: [''' + orgtime + ''']
275+
:ID: ''' + orgdate + '''-''' + short + '''-confidence-green
276+
:BLOCKER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria ''' + \
277+
orgdate + '''-''' + short + '''-estimation
278+
:TRIGGER:
279+
:END:
280+
281+
*** TODO hand over to team
282+
:PROPERTIES:
283+
:CREATED: [''' + orgtime + ''']
284+
:BLOCKER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria ''' + \
285+
orgdate + '''-''' + short + '''-estimation
286+
:ID: ''' + orgdate + '''-''' + short + '''-hand-over-team
287+
:TRIGGER: ''' + orgdate + '''-''' + short + '''-accept(WAITING) ''' + \
288+
orgdate + '''-Story-''' + short + '''(TEAM)
289+
:END:
290+
291+
*** acceptance + finish US
292+
:PROPERTIES:
293+
:CREATED: [''' + orgtime + ''']
294+
:ID: ''' + orgdate + '''-''' + short + '''-accept
295+
:BLOCKER: ''' + orgdate + '''-''' + short + '''-hand-over-team
296+
:TRIGGER: ''' + orgdate + '''-Story-''' + short + '''(DONE)
297+
:END:
298+
''')
299+
300+
301+
def main():
302+
"""Main function"""
303+
304+
ipds = []
305+
for argument in args.IPD:
306+
for ipd in argument.split(' '):
307+
## maybe, there is only one argument like "1234 2345" which needs to be splitted:
308+
ipd_int = int(ipd)
309+
ipds.append(str(ipd_int)) # make sure that there are only numbers
310+
311+
issues = retrieve_data_from_jira(ipds)
312+
313+
for issue in issues:
314+
print_issue(issue)
315+
316+
317+
if __name__ == "__main__":
318+
try:
319+
main()
320+
except KeyboardInterrupt:
321+
322+
logging.info("Received KeyboardInterrupt")
323+
324+
# END OF FILE #################################################################

0 commit comments

Comments
 (0)