releasedocmaker.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. #!/usr/bin/env python
  2. #
  3. # Licensed to the Apache Software Foundation (ASF) under one
  4. # or more contributor license agreements. See the NOTICE file
  5. # distributed with this work for additional information
  6. # regarding copyright ownership. The ASF licenses this file
  7. # to you under the Apache License, Version 2.0 (the
  8. # "License"); you may not use this file except in compliance
  9. # with the License. You may obtain a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. from glob import glob
  19. from optparse import OptionParser
  20. import os
  21. import re
  22. import sys
  23. import urllib
  24. try:
  25. import json
  26. except ImportError:
  27. import simplejson as json
  28. releaseVersion={}
  29. namePattern = re.compile(r' \([0-9]+\)')
  30. def clean(str):
  31. return tableclean(re.sub(namePattern, "", str))
  32. def formatComponents(str):
  33. str = re.sub(namePattern, '', str).replace("'", "")
  34. if str != "":
  35. ret = str
  36. else:
  37. # some markdown parsers don't like empty tables
  38. ret = "."
  39. return clean(ret)
  40. # convert to utf-8
  41. # protect some known md metachars
  42. # or chars that screw up doxia
  43. def tableclean(str):
  44. str=str.encode('utf-8')
  45. str=str.replace("_","\_")
  46. str=str.replace("\r","")
  47. str=str.rstrip()
  48. return str
  49. # same thing as tableclean,
  50. # except table metachars are also
  51. # escaped as well as more
  52. # things we don't want doxia to
  53. # screw up
  54. def notableclean(str):
  55. str=tableclean(str)
  56. str=str.replace("|","\|")
  57. str=str.replace("<","\<")
  58. str=str.replace(">","\>")
  59. str=str.rstrip()
  60. return str
  61. def mstr(obj):
  62. if (obj == None):
  63. return ""
  64. return unicode(obj)
  65. def buildindex(master):
  66. versions=reversed(sorted(glob("[0-9]*.[0-9]*.[0-9]*")))
  67. with open("index.md","w") as indexfile:
  68. for v in versions:
  69. indexfile.write("* Apache Hadoop v%s\n" % (v))
  70. for k in ("Changes","Release Notes"):
  71. indexfile.write(" * %s\n" %(k))
  72. indexfile.write(" * [Combined %s](%s/%s.%s.html)\n" \
  73. % (k,v,k.upper().replace(" ",""),v))
  74. if not master:
  75. indexfile.write(" * [Hadoop Common %s](%s/%s.HADOOP.%s.html)\n" \
  76. % (k,v,k.upper().replace(" ",""),v))
  77. for p in ("HDFS","MapReduce","YARN"):
  78. indexfile.write(" * [%s %s](%s/%s.%s.%s.html)\n" \
  79. % (p,k,v,k.upper().replace(" ",""),p.upper(),v))
  80. indexfile.close()
  81. class Version:
  82. """Represents a version number"""
  83. def __init__(self, data):
  84. self.mod = False
  85. self.data = data
  86. found = re.match('^((\d+)(\.\d+)*).*$', data)
  87. if (found):
  88. self.parts = [ int(p) for p in found.group(1).split('.') ]
  89. else:
  90. self.parts = []
  91. # backfill version with zeroes if missing parts
  92. self.parts.extend((0,) * (3 - len(self.parts)))
  93. def __str__(self):
  94. if (self.mod):
  95. return '.'.join([ str(p) for p in self.parts ])
  96. return self.data
  97. def __cmp__(self, other):
  98. return cmp(self.parts, other.parts)
  99. class Jira:
  100. """A single JIRA"""
  101. def __init__(self, data, parent):
  102. self.key = data['key']
  103. self.fields = data['fields']
  104. self.parent = parent
  105. self.notes = None
  106. self.incompat = None
  107. self.reviewed = None
  108. def getId(self):
  109. return mstr(self.key)
  110. def getDescription(self):
  111. return mstr(self.fields['description'])
  112. def getReleaseNote(self):
  113. if (self.notes == None):
  114. field = self.parent.fieldIdMap['Release Note']
  115. if (self.fields.has_key(field)):
  116. self.notes=mstr(self.fields[field])
  117. else:
  118. self.notes=self.getDescription()
  119. return self.notes
  120. def getPriority(self):
  121. ret = ""
  122. pri = self.fields['priority']
  123. if(pri != None):
  124. ret = pri['name']
  125. return mstr(ret)
  126. def getAssignee(self):
  127. ret = ""
  128. mid = self.fields['assignee']
  129. if(mid != None):
  130. ret = mid['displayName']
  131. return mstr(ret)
  132. def getComponents(self):
  133. if (len(self.fields['components'])>0):
  134. return ", ".join([ comp['name'] for comp in self.fields['components'] ])
  135. else:
  136. return ""
  137. def getSummary(self):
  138. return self.fields['summary']
  139. def getType(self):
  140. ret = ""
  141. mid = self.fields['issuetype']
  142. if(mid != None):
  143. ret = mid['name']
  144. return mstr(ret)
  145. def getReporter(self):
  146. ret = ""
  147. mid = self.fields['reporter']
  148. if(mid != None):
  149. ret = mid['displayName']
  150. return mstr(ret)
  151. def getProject(self):
  152. ret = ""
  153. mid = self.fields['project']
  154. if(mid != None):
  155. ret = mid['key']
  156. return mstr(ret)
  157. def __cmp__(self,other):
  158. selfsplit=self.getId().split('-')
  159. othersplit=other.getId().split('-')
  160. v1=cmp(selfsplit[0],othersplit[0])
  161. if (v1!=0):
  162. return v1
  163. else:
  164. if selfsplit[1] < othersplit[1]:
  165. return True
  166. elif selfsplit[1] > othersplit[1]:
  167. return False
  168. return False
  169. def getIncompatibleChange(self):
  170. if (self.incompat == None):
  171. field = self.parent.fieldIdMap['Hadoop Flags']
  172. self.reviewed=False
  173. self.incompat=False
  174. if (self.fields.has_key(field)):
  175. if self.fields[field]:
  176. for hf in self.fields[field]:
  177. if hf['value'] == "Incompatible change":
  178. self.incompat=True
  179. if hf['value'] == "Reviewed":
  180. self.reviewed=True
  181. return self.incompat
  182. def getReleaseDate(self,version):
  183. for j in range(len(self.fields['fixVersions'])):
  184. if self.fields['fixVersions'][j]==version:
  185. return(self.fields['fixVersions'][j]['releaseDate'])
  186. return None
  187. class JiraIter:
  188. """An Iterator of JIRAs"""
  189. def __init__(self, versions):
  190. self.versions = versions
  191. resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/field")
  192. data = json.loads(resp.read())
  193. self.fieldIdMap = {}
  194. for part in data:
  195. self.fieldIdMap[part['name']] = part['id']
  196. self.jiras = []
  197. at=0
  198. end=1
  199. count=100
  200. while (at < end):
  201. params = urllib.urlencode({'jql': "project in (HADOOP,HDFS,MAPREDUCE,YARN) and fixVersion in ('"+"' , '".join([str(v).replace("-SNAPSHOT","") for v in versions])+"') and resolution = Fixed", 'startAt':at, 'maxResults':count})
  202. resp = urllib.urlopen("https://issues.apache.org/jira/rest/api/2/search?%s"%params)
  203. data = json.loads(resp.read())
  204. if (data.has_key('errorMessages')):
  205. raise Exception(data['errorMessages'])
  206. at = data['startAt'] + data['maxResults']
  207. end = data['total']
  208. self.jiras.extend(data['issues'])
  209. needaversion=False
  210. for j in versions:
  211. v=str(j).replace("-SNAPSHOT","")
  212. if v not in releaseVersion:
  213. needaversion=True
  214. if needaversion is True:
  215. for i in range(len(data['issues'])):
  216. for j in range(len(data['issues'][i]['fields']['fixVersions'])):
  217. if 'releaseDate' in data['issues'][i]['fields']['fixVersions'][j]:
  218. releaseVersion[data['issues'][i]['fields']['fixVersions'][j]['name']]=\
  219. data['issues'][i]['fields']['fixVersions'][j]['releaseDate']
  220. self.iter = self.jiras.__iter__()
  221. def __iter__(self):
  222. return self
  223. def next(self):
  224. data = self.iter.next()
  225. j = Jira(data, self)
  226. return j
  227. class Outputs:
  228. """Several different files to output to at the same time"""
  229. def __init__(self, base_file_name, file_name_pattern, keys, params={}):
  230. self.params = params
  231. self.base = open(base_file_name%params, 'w')
  232. self.others = {}
  233. for key in keys:
  234. both = dict(params)
  235. both['key'] = key
  236. self.others[key] = open(file_name_pattern%both, 'w')
  237. def writeAll(self, pattern):
  238. both = dict(self.params)
  239. both['key'] = ''
  240. self.base.write(pattern%both)
  241. for key in self.others.keys():
  242. both = dict(self.params)
  243. both['key'] = key
  244. self.others[key].write(pattern%both)
  245. def writeKeyRaw(self, key, str):
  246. self.base.write(str)
  247. if (self.others.has_key(key)):
  248. self.others[key].write(str)
  249. def close(self):
  250. self.base.close()
  251. for fd in self.others.values():
  252. fd.close()
  253. def writeList(self, mylist):
  254. for jira in sorted(mylist):
  255. line = '| [%s](https://issues.apache.org/jira/browse/%s) | %s | %s | %s | %s | %s |\n' \
  256. % (notableclean(jira.getId()), notableclean(jira.getId()),
  257. notableclean(jira.getSummary()),
  258. notableclean(jira.getPriority()),
  259. formatComponents(jira.getComponents()),
  260. notableclean(jira.getReporter()),
  261. notableclean(jira.getAssignee()))
  262. self.writeKeyRaw(jira.getProject(), line)
  263. def main():
  264. parser = OptionParser(usage="usage: %prog --version VERSION [--version VERSION2 ...]",
  265. epilog=
  266. "Markdown-formatted CHANGES and RELEASENOTES files will be stored in a directory"
  267. " named after the highest version provided.")
  268. parser.add_option("-v", "--version", dest="versions",
  269. action="append", type="string",
  270. help="versions in JIRA to include in releasenotes", metavar="VERSION")
  271. parser.add_option("-m","--master", dest="master", action="store_true",
  272. help="only create the master, merged project files")
  273. parser.add_option("-i","--index", dest="index", action="store_true",
  274. help="build an index file")
  275. (options, args) = parser.parse_args()
  276. if (options.versions == None):
  277. options.versions = []
  278. if (len(args) > 2):
  279. options.versions.append(args[2])
  280. if (len(options.versions) <= 0):
  281. parser.error("At least one version needs to be supplied")
  282. versions = [ Version(v) for v in options.versions ];
  283. versions.sort();
  284. maxVersion = str(versions[-1])
  285. jlist = JiraIter(versions)
  286. version = maxVersion
  287. if version in releaseVersion:
  288. reldate=releaseVersion[version]
  289. else:
  290. reldate="Unreleased"
  291. if not os.path.exists(version):
  292. os.mkdir(version)
  293. if options.master:
  294. reloutputs = Outputs("%(ver)s/RELEASENOTES.%(ver)s.md",
  295. "%(ver)s/RELEASENOTES.%(key)s.%(ver)s.md",
  296. [], {"ver":maxVersion, "date":reldate})
  297. choutputs = Outputs("%(ver)s/CHANGES.%(ver)s.md",
  298. "%(ver)s/CHANGES.%(key)s.%(ver)s.md",
  299. [], {"ver":maxVersion, "date":reldate})
  300. else:
  301. reloutputs = Outputs("%(ver)s/RELEASENOTES.%(ver)s.md",
  302. "%(ver)s/RELEASENOTES.%(key)s.%(ver)s.md",
  303. ["HADOOP","HDFS","MAPREDUCE","YARN"], {"ver":maxVersion, "date":reldate})
  304. choutputs = Outputs("%(ver)s/CHANGES.%(ver)s.md",
  305. "%(ver)s/CHANGES.%(key)s.%(ver)s.md",
  306. ["HADOOP","HDFS","MAPREDUCE","YARN"], {"ver":maxVersion, "date":reldate})
  307. relhead = '# Hadoop %(key)s %(ver)s Release Notes\n\n' \
  308. 'These release notes cover new developer and user-facing incompatibilities, features, and major improvements.\n\n'
  309. chhead = '# Hadoop Changelog\n\n' \
  310. '## Release %(ver)s - %(date)s\n'\
  311. '\n'
  312. reloutputs.writeAll(relhead)
  313. choutputs.writeAll(chhead)
  314. incompatlist=[]
  315. buglist=[]
  316. improvementlist=[]
  317. newfeaturelist=[]
  318. subtasklist=[]
  319. tasklist=[]
  320. testlist=[]
  321. otherlist=[]
  322. for jira in sorted(jlist):
  323. if jira.getIncompatibleChange():
  324. incompatlist.append(jira)
  325. elif jira.getType() == "Bug":
  326. buglist.append(jira)
  327. elif jira.getType() == "Improvement":
  328. improvementlist.append(jira)
  329. elif jira.getType() == "New Feature":
  330. newfeaturelist.append(jira)
  331. elif jira.getType() == "Sub-task":
  332. subtasklist.append(jira)
  333. elif jira.getType() == "Task":
  334. tasklist.append(jira)
  335. elif jira.getType() == "Test":
  336. testlist.append(jira)
  337. else:
  338. otherlist.append(jira)
  339. line = '* [%s](https://issues.apache.org/jira/browse/%s) | *%s* | **%s**\n' \
  340. % (notableclean(jira.getId()), notableclean(jira.getId()), notableclean(jira.getPriority()),
  341. notableclean(jira.getSummary()))
  342. if (jira.getIncompatibleChange()) and (len(jira.getReleaseNote())==0):
  343. reloutputs.writeKeyRaw(jira.getProject(),"\n---\n\n")
  344. reloutputs.writeKeyRaw(jira.getProject(), line)
  345. line ='\n**WARNING: No release note provided for this incompatible change.**\n\n'
  346. print 'WARNING: incompatible change %s lacks release notes.' % (notableclean(jira.getId()))
  347. reloutputs.writeKeyRaw(jira.getProject(), line)
  348. if (len(jira.getReleaseNote())>0):
  349. reloutputs.writeKeyRaw(jira.getProject(),"\n---\n\n")
  350. reloutputs.writeKeyRaw(jira.getProject(), line)
  351. line ='\n%s\n\n' % (tableclean(jira.getReleaseNote()))
  352. reloutputs.writeKeyRaw(jira.getProject(), line)
  353. reloutputs.writeAll("\n\n")
  354. reloutputs.close()
  355. choutputs.writeAll("### INCOMPATIBLE CHANGES:\n\n")
  356. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  357. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  358. choutputs.writeList(incompatlist)
  359. choutputs.writeAll("\n\n### NEW FEATURES:\n\n")
  360. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  361. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  362. choutputs.writeList(newfeaturelist)
  363. choutputs.writeAll("\n\n### IMPROVEMENTS:\n\n")
  364. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  365. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  366. choutputs.writeList(improvementlist)
  367. choutputs.writeAll("\n\n### BUG FIXES:\n\n")
  368. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  369. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  370. choutputs.writeList(buglist)
  371. choutputs.writeAll("\n\n### TESTS:\n\n")
  372. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  373. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  374. choutputs.writeList(testlist)
  375. choutputs.writeAll("\n\n### SUB-TASKS:\n\n")
  376. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  377. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  378. choutputs.writeList(subtasklist)
  379. choutputs.writeAll("\n\n### OTHER:\n\n")
  380. choutputs.writeAll("| JIRA | Summary | Priority | Component | Reporter | Contributor |\n")
  381. choutputs.writeAll("|:---- |:---- | :--- |:---- |:---- |:---- |\n")
  382. choutputs.writeList(otherlist)
  383. choutputs.writeList(tasklist)
  384. choutputs.writeAll("\n\n")
  385. choutputs.close()
  386. if options.index:
  387. buildindex(options.master)
  388. if __name__ == "__main__":
  389. main()