hueJira.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const util = require('util');
  2. const exec = util.promisify(require('child_process').exec);
  3. const JiraApi = require('jira-client');
  4. require('colors');
  5. const program = require('commander');
  6. program
  7. .version('1.0')
  8. .option('-r, --rbtUser [rbtUser]', 'The Review Board user')
  9. .option('-j --jiraUser [jiraUser]', 'The Jira user')
  10. .option('-p --jiraPassword [jiraPassword]', 'The Jira password')
  11. .option(
  12. '-h --huePath [huePath]',
  13. 'The path to the Hue folder, if not set it will be ../../ relative to the tool (optional)'
  14. )
  15. .option('-d --dryRun', 'Will not update any Jiras (optional)')
  16. .parse(process.argv);
  17. const missing = [];
  18. if (!program.rbtUser) {
  19. missing.push('rbtUser');
  20. }
  21. if (!program.jiraUser) {
  22. missing.push('jiraUser');
  23. }
  24. if (!program.jiraPassword) {
  25. missing.push('jiraPassword');
  26. }
  27. if (missing.length) {
  28. // eslint-disable-next-line no-restricted-syntax
  29. console.log(`\nThe following required arguments are missing:\n ${missing.join('\n ').red}\n`);
  30. program.help();
  31. }
  32. const getPendingReviews = async user => {
  33. const { stdout, stderr } = await exec(
  34. `rbt api-get --server=https://review.cloudera.org /review-requests/ --from-user=${user} --status=pending --max-results=20`
  35. );
  36. if (stderr) {
  37. throw new Error(stderr);
  38. }
  39. return JSON.parse(stdout).review_requests;
  40. };
  41. const getCommits = async (jira, huePath) => {
  42. const { stdout, stderr } = await exec(
  43. `pushd ${huePath} > /dev/null && git log origin/master --grep=${jira} --format=https://github.com/cloudera/hue/commit/%H | tail -r && popd > /dev/null`
  44. );
  45. if (stderr) {
  46. throw new Error(stderr);
  47. }
  48. if (!stdout.trim()) {
  49. return [];
  50. }
  51. return stdout.trim().split('\n');
  52. };
  53. const extractJiras = reviews => {
  54. const jiraIndex = {};
  55. reviews.forEach(review => {
  56. const matches = review.description.match(/(HUE-[0-9]+)/gi);
  57. if (matches) {
  58. matches.forEach(jira => {
  59. const upperJira = jira.toUpperCase();
  60. if (!jiraIndex[upperJira]) {
  61. jiraIndex[upperJira] = { reviewUrls: {} };
  62. }
  63. jiraIndex[upperJira].reviewUrls[review.absolute_url] = true;
  64. });
  65. }
  66. });
  67. const result = [];
  68. Object.keys(jiraIndex).forEach(jiraNumber => {
  69. const urls = Object.keys(jiraIndex[jiraNumber].reviewUrls);
  70. urls.sort();
  71. result.push({
  72. jira: jiraNumber,
  73. reviews: urls
  74. });
  75. });
  76. result.sort((a, b) => {
  77. return a.jira.localeCompare(b.jira);
  78. });
  79. return result;
  80. };
  81. const asyncForEach = async (array, callback) => {
  82. for (let index = 0; index < array.length; index++) {
  83. await callback(array[index], index, array);
  84. }
  85. };
  86. const jiraApi = new JiraApi({
  87. protocol: 'https',
  88. host: 'issues.cloudera.org/',
  89. username: program.jiraUser,
  90. password: program.jiraPassword,
  91. apiVersion: '2',
  92. strictSSL: true
  93. });
  94. const fetchExistingJira = async jiraNumber => {
  95. const existingJira = await jiraApi.findIssue(jiraNumber, undefined, 'comment, status');
  96. if (
  97. !existingJira ||
  98. !existingJira.fields ||
  99. !existingJira.fields.comment ||
  100. !existingJira.fields.status
  101. ) {
  102. throw new Error('Returned Jira has no comment or status fields.');
  103. }
  104. return existingJira;
  105. };
  106. const extractJiraComment = existingJira => {
  107. if (existingJira.fields.comment.comments) {
  108. return existingJira.fields.comment.comments.reduce(
  109. (val, comment) => val + comment.body + '\n',
  110. ''
  111. );
  112. }
  113. return '';
  114. };
  115. const hasOpenStatus = existingJira => existingJira.fields.status.id === '1';
  116. const generateComment = (commitUrls, reviewUrls) => {
  117. let comment = '';
  118. if (reviewUrls.length) {
  119. comment += reviewUrls.length > 1 ? 'Reviews:\n' : 'Review:\n';
  120. comment += reviewUrls.join('\n');
  121. }
  122. if (commitUrls.length) {
  123. if (comment) {
  124. comment += '\n\n';
  125. }
  126. comment += commitUrls.length > 1 ? 'Commits:\n' : 'Commit:\n';
  127. comment += commitUrls.join('\n');
  128. }
  129. return comment;
  130. };
  131. const transitionJiraToInProgress = async jira => {
  132. await jiraApi.transitionIssue(jira.key, { transition: { id: 4 } });
  133. };
  134. const updateExistingJira = async reviewJiras => {
  135. let updatedJiraCount = 0;
  136. await asyncForEach(reviewJiras, async reviewJira => {
  137. const missingCommitUrls = [];
  138. const missingReviewUrls = [];
  139. const existingJira = await fetchExistingJira(reviewJira.jira);
  140. const existingComment = extractJiraComment(existingJira).toLowerCase();
  141. reviewJira.reviews.forEach(review => {
  142. if (existingComment.indexOf(review.toLowerCase()) === -1) {
  143. missingReviewUrls.push(review);
  144. }
  145. });
  146. reviewJira.commits.forEach(commit => {
  147. if (existingComment.indexOf(commit.toLowerCase()) === -1) {
  148. missingCommitUrls.push(commit);
  149. }
  150. });
  151. if (missingCommitUrls.length || missingReviewUrls.length) {
  152. const comment = generateComment(missingCommitUrls, missingReviewUrls);
  153. // eslint-disable-next-line no-restricted-syntax
  154. console.log(`\nAdding comment to ${existingJira.key}...\n`.cyan);
  155. // eslint-disable-next-line no-restricted-syntax
  156. console.log(comment);
  157. updatedJiraCount++;
  158. if (!program.dryRun) {
  159. await jiraApi.addComment(existingJira.key, comment);
  160. }
  161. if (hasOpenStatus(existingJira)) {
  162. // eslint-disable-next-line no-restricted-syntax
  163. console.log(`\nTransitioning ${existingJira.key} to 'In Progress'...\n`.cyan);
  164. if (!program.dryRun) {
  165. await transitionJiraToInProgress(existingJira);
  166. }
  167. }
  168. }
  169. });
  170. return updatedJiraCount;
  171. };
  172. const HUE_PATH = program.huePath || `${__dirname}/../../`;
  173. const linkCommits = async reviewJiras => {
  174. await asyncForEach(reviewJiras, async reviewJira => {
  175. reviewJira.commits = await getCommits(reviewJira.jira, HUE_PATH);
  176. });
  177. };
  178. const main = async () => {
  179. // Fetch the reviews using rbt cli
  180. const reviews = await getPendingReviews(program.rbtUser);
  181. // Extract jira information form the reviews
  182. const reviewJiras = extractJiras(reviews);
  183. // Use git log orgin/master and link commits to the jiras from the reviews
  184. await linkCommits(reviewJiras);
  185. // Update the actual jira with missing review and commit urls
  186. const updatedJiraCount = await updateExistingJira(reviewJiras);
  187. if (updatedJiraCount) {
  188. // eslint-disable-next-line no-restricted-syntax
  189. console.log(
  190. `\nUpdated ${updatedJiraCount} jira(s)${program.dryRun ? ' (dry run)'.gray : ''}`.green
  191. );
  192. } else {
  193. // eslint-disable-next-line no-restricted-syntax
  194. console.log('\nNothing to update.\n'.green);
  195. }
  196. };
  197. try {
  198. main();
  199. } catch (err) {
  200. console.error(err);
  201. }