releasedocmaker.py 15 KB

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