releasedocmaker.py 15 KB

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