const util = require('util'); const exec = util.promisify(require('child_process').exec); const JiraApi = require('jira-client'); require('colors'); const program = require('commander'); program .version('1.0') .option('-r, --rbtUser [rbtUser]', 'The Review Board user') .option('-j --jiraUser [jiraUser]', 'The Jira user') .option('-p --jiraPassword [jiraPassword]', 'The Jira password') .option( '-h --huePath [huePath]', 'The path to the Hue folder, if not set it will be ../../ relative to the tool (optional)' ) .option('-d --dryRun', 'Will not update any Jiras (optional)') .parse(process.argv); const missing = []; if (!program.rbtUser) { missing.push('rbtUser'); } if (!program.jiraUser) { missing.push('jiraUser'); } if (!program.jiraPassword) { missing.push('jiraPassword'); } if (missing.length) { // eslint-disable-next-line no-restricted-syntax console.log(`\nThe following required arguments are missing:\n ${missing.join('\n ').red}\n`); program.help(); } const getPendingReviews = async user => { const { stdout, stderr } = await exec( `rbt api-get --server=https://review.cloudera.org /review-requests/ --from-user=${user} --status=pending --max-results=100` ); if (stderr) { throw new Error(stderr); } return JSON.parse(stdout).review_requests; }; const getCommits = async (jira, huePath) => { const { stdout, stderr } = await exec( `pushd ${huePath} > /dev/null && git log origin/master --grep=${jira} --format=https://github.com/cloudera/hue/commit/%H | tail -r && popd > /dev/null` ); if (stderr) { throw new Error(stderr); } if (!stdout.trim()) { return []; } return stdout.trim().split('\n'); }; const extractJiras = reviews => { const jiraIndex = {}; reviews.forEach(review => { const matches = review.description.match(/(HUE-[0-9]+)/gi); if (matches) { matches.forEach(jira => { const upperJira = jira.toUpperCase(); if (!jiraIndex[upperJira]) { jiraIndex[upperJira] = { reviewUrls: {} }; } jiraIndex[upperJira].reviewUrls[review.absolute_url] = true; }); } }); const result = []; Object.keys(jiraIndex).forEach(jiraNumber => { const urls = Object.keys(jiraIndex[jiraNumber].reviewUrls); urls.sort(); result.push({ jira: jiraNumber, reviews: urls }); }); result.sort((a, b) => { return a.jira.localeCompare(b.jira); }); return result; }; const asyncForEach = async (array, callback) => { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } }; const jiraApi = new JiraApi({ protocol: 'https', host: 'issues.cloudera.org/', username: program.jiraUser, password: program.jiraPassword, apiVersion: '2', strictSSL: true }); const fetchExistingJiraComments = async jiraNumber => { const jira = await jiraApi.findIssue(jiraNumber, undefined, 'comment'); if (jira && jira.fields && jira.fields.comment && jira.fields.comment.comments) { return jira.fields.comment.comments.reduce((val, comment) => val + comment.body + '\n', ''); } return ''; }; const generateComment = (commitUrls, reviewUrls) => { let comment = ''; if (reviewUrls.length) { comment += reviewUrls.length > 1 ? 'Reviews:\n' : 'Review:\n'; comment += reviewUrls.join('\n'); } if (commitUrls.length) { if (comment) { comment += '\n\n'; } comment += commitUrls.length > 1 ? 'Commits:\n' : 'Commit:\n'; comment += commitUrls.join('\n'); } return comment; }; const setMissingJiraComments = async reviewJiras => { let updatedJiraCount = 0; await asyncForEach(reviewJiras, async reviewJira => { const missingCommitUrls = []; const missingReviewUrls = []; const existingComment = (await fetchExistingJiraComments(reviewJira.jira)).toLowerCase(); reviewJira.reviews.forEach(review => { if (existingComment.indexOf(review.toLowerCase()) === -1) { missingReviewUrls.push(review); } }); reviewJira.commits.forEach(commit => { if (existingComment.indexOf(commit.toLowerCase()) === -1) { missingCommitUrls.push(commit); } }); if (missingCommitUrls.length || missingReviewUrls.length) { const comment = generateComment(missingCommitUrls, missingReviewUrls); // eslint-disable-next-line no-restricted-syntax console.log(`\nAdding to ${reviewJira.jira}...\n`.cyan); // eslint-disable-next-line no-restricted-syntax console.log(comment); updatedJiraCount++; if (!program.dryRun) { await jiraApi.addComment(reviewJira.jira, comment); } } }); return updatedJiraCount; }; const HUE_PATH = program.huePath || `${__dirname}/../../`; const linkCommits = async reviewJiras => { await asyncForEach(reviewJiras, async reviewJira => { reviewJira.commits = await getCommits(reviewJira.jira, HUE_PATH); }); }; const main = async () => { // Fetch the reviews using rbt cli const reviews = await getPendingReviews(program.rbtUser); // Extract jira information form the reviews const reviewJiras = extractJiras(reviews); // Use git log orgin/master and link commits to the jiras from the reviews await linkCommits(reviewJiras); // Update the actual jira with missing review and commit urls const updatedJiraCount = await setMissingJiraComments(reviewJiras); if (updatedJiraCount) { // eslint-disable-next-line no-restricted-syntax console.log( `\nUpdated ${updatedJiraCount} jira(s)${program.dryRun ? ' (dry run)'.gray : ''}`.green ); } else { // eslint-disable-next-line no-restricted-syntax console.log('\nNothing to update.\n'.green); } }; try { main(); } catch (err) { console.error(err); }