hueJira.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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=100`
  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 fetchExistingJiraComments = async jiraNumber => {
  95. const jira = await jiraApi.findIssue(jiraNumber, undefined, 'comment');
  96. if (jira && jira.fields && jira.fields.comment && jira.fields.comment.comments) {
  97. return jira.fields.comment.comments.reduce((val, comment) => val + comment.body + '\n', '');
  98. }
  99. return '';
  100. };
  101. const generateComment = (commitUrls, reviewUrls) => {
  102. let comment = '';
  103. if (reviewUrls.length) {
  104. comment += reviewUrls.length > 1 ? 'Reviews:\n' : 'Review:\n';
  105. comment += reviewUrls.join('\n');
  106. }
  107. if (commitUrls.length) {
  108. if (comment) {
  109. comment += '\n\n';
  110. }
  111. comment += commitUrls.length > 1 ? 'Commits:\n' : 'Commit:\n';
  112. comment += commitUrls.join('\n');
  113. }
  114. return comment;
  115. };
  116. const setMissingJiraComments = async reviewJiras => {
  117. let updatedJiraCount = 0;
  118. await asyncForEach(reviewJiras, async reviewJira => {
  119. const missingCommitUrls = [];
  120. const missingReviewUrls = [];
  121. const existingComment = (await fetchExistingJiraComments(reviewJira.jira)).toLowerCase();
  122. reviewJira.reviews.forEach(review => {
  123. if (existingComment.indexOf(review.toLowerCase()) === -1) {
  124. missingReviewUrls.push(review);
  125. }
  126. });
  127. reviewJira.commits.forEach(commit => {
  128. if (existingComment.indexOf(commit.toLowerCase()) === -1) {
  129. missingCommitUrls.push(commit);
  130. }
  131. });
  132. if (missingCommitUrls.length || missingReviewUrls.length) {
  133. const comment = generateComment(missingCommitUrls, missingReviewUrls);
  134. // eslint-disable-next-line no-restricted-syntax
  135. console.log(`\nAdding to ${reviewJira.jira}...\n`.cyan);
  136. // eslint-disable-next-line no-restricted-syntax
  137. console.log(comment);
  138. updatedJiraCount++;
  139. if (!program.dryRun) {
  140. await jiraApi.addComment(reviewJira.jira, comment);
  141. }
  142. }
  143. });
  144. return updatedJiraCount;
  145. };
  146. const HUE_PATH = program.huePath || `${__dirname}/../../`;
  147. const linkCommits = async reviewJiras => {
  148. await asyncForEach(reviewJiras, async reviewJira => {
  149. reviewJira.commits = await getCommits(reviewJira.jira, HUE_PATH);
  150. });
  151. };
  152. const main = async () => {
  153. // Fetch the reviews using rbt cli
  154. const reviews = await getPendingReviews(program.rbtUser);
  155. // Extract jira information form the reviews
  156. const reviewJiras = extractJiras(reviews);
  157. // Use git log orgin/master and link commits to the jiras from the reviews
  158. await linkCommits(reviewJiras);
  159. // Update the actual jira with missing review and commit urls
  160. const updatedJiraCount = await setMissingJiraComments(reviewJiras);
  161. if (updatedJiraCount) {
  162. // eslint-disable-next-line no-restricted-syntax
  163. console.log(
  164. `\nUpdated ${updatedJiraCount} jira(s)${program.dryRun ? ' (dry run)'.gray : ''}`.green
  165. );
  166. } else {
  167. // eslint-disable-next-line no-restricted-syntax
  168. console.log('\nNothing to update.\n'.green);
  169. }
  170. };
  171. try {
  172. main();
  173. } catch (err) {
  174. console.error(err);
  175. }