#!/usr/bin/env /usr/local/bin/node const https = require('https'); /* EDIT HERE */ /** Domain only, no protocol. Eg. company.atlassian.net */ const BASE_URL = ''; const USER = ''; const API_TOKEN = ''; const PROJECT_KEY = ''; const LIMIT = 10; const IGNORED_STATUSES = ['created', 'done', 'resolved', '"Code Review"']; const SHOW_TIMETRACK = true; const SHOW_ALL_ISSUES = false; /* DON'T EDIT BELOW */ // Jira // v1.0 // Gil Barbara // gilbarbara // List tickets. // node // https://github.com/gilbarbara/bitbar-plugins const AUTH = Buffer.from(`${USER}:${API_TOKEN}`).toString('base64'); const BROWSE_URL = `https://${BASE_URL}/browse/`; const ICON = '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IFF1YXJ0eiBQREZDb250ZXh0KQplbmRvYmoKMTIgMCBvYmoKKEQ6MjAxODA4MTYwMDI2MjJaMDAnMDAnKQplbmRvYmoKMSAwIG9iago8PCAvUHJvZHVjZXIgMTEgMCBSIC9DcmVhdGlvbkRhdGUgMTIgMCBSIC9Nb2REYXRlIDEyIDAgUiA+PgplbmRvYmoKeHJlZgowIDEzCjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMzY2OSAwMDAwMCBuIAowMDAwMDAwNTI3IDAwMDAwIG4gCjAwMDAwMDM0NDMgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBuIAowMDAwMDAwNTA4IDAwMDAwIG4gCjAwMDAwMDA2MDcgMDAwMDAgbiAKMDAwMDAwMzQwOCAwMDAwMCBuIAowMDAwMDAwNjc1IDAwMDAwIG4gCjAwMDAwMDMzODggMDAwMDAgbiAKMDAwMDAwMzUyNCAwMDAwMCBuIAowMDAwMDAzNTc0IDAwMDAwIG4gCjAwMDAwMDM2MjcgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSAxMyAvUm9vdCAxMCAwIFIgL0luZm8gMSAwIFIgL0lEIFsgPDY4NjJkZmMxZDVhNzFmNDFmYmIwM2RlMmU3NmM1YWNhPgo8Njg2MmRmYzFkNWE3MWY0MWZiYjAzZGUyZTc2YzVhY2E+IF0gPj4Kc3RhcnR4cmVmCjM3NDQKJSVFT0YK'; const ICONS = { Bug: 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAZ0lEQVR4AWN47mnt+dTT6jEQ/ycRPwbpZSBPM8IQBhCDEkw7A56He/1/21IFwiA2aQa8qS/9//fL5/9QAGKDxAgYgLAZWTOyISA5ggaAnYwDgOQoNoBiL1AUiPSJRvqnRIozE8XZGQBnKZxDdHA1vgAAAABJRU5ErkJggg==', Epic: 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAk0lEQVR4AWOYHPDYc6L/48cT/B79JwWD9ID0MiBrJscQBhCDEozTgMkhj//vn/mOfAMOznn3HwhINQBh+5f3f8g34OBcoO1QMD/tGRQ//T/BnwgDpsBtxwRbO18TNACsCBv4CjQUaDhBA0DOBDkX7nQYODT3PQmxgMDItpNpAMJ28gzYP+MdKFrxG0AJpjgzUZydAYNC27BT2FzcAAAAAElFTkSuQmCC', Feature: 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAHlBMVEVptklotUhotEeazYV0ulb9/v19vmD///+Px3d1ulYbdfwGAAAAAnRSTlNJ424rirYAAABBSURBVHgBY2BUAgMBBiEIQ5FBCQpADGUjKMM1BMpIL4Mw3NorUkAM5fDy8lIjZIbSNKgUQrHS1BBUA6EAbincGQCn+xff/fpqagAAAABJRU5ErkJggg==', Story: 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAi0lEQVR4AWNI3WvvmbLL9nHyLpv/pGCQHpBeBmTN5BjCAGJQgjEMyNjj9H/1ren/9zxcjYxBYiA5wgbMvtT0HwcAyRE2YOn1flz6QXIjzoAvvz6BMHkG/Pzz43/LiTQQBrFJM+Dvv7//p16ogomB2CAx4gyoOhIJ1FCNrhAkBpLDbgAlmOLMRHF2BgAy2aynfVwqNAAAAABJRU5ErkJggg==', 'Sub-task': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAOVBMVEVLrulNr+pkuetLruhiuOpZtOn///+PzPD9/v7L5/jJ5vhNruii1fPY7fqX0PHh8fq/4vbw+PxVsukX2Ns6AAAAAnRSTlPjSQzAlCMAAABjSURBVHjaZc/ZFoAgCEVRQ8R56v8/NqSVVp4n7n5DHUq/4il7JTsiAJhHeGfinJ2AdPKRwgSgcRT/AWy1+2AXNBo5I2BcKpUkENA2+P4Cyf8h3IATrBs7RwHJAADy1ttz2/sXXRcEbjp7uW8AAAAASUVORK5CYII=', Task: 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAyElEQVR4AWPwXf/K03vdy8fea1/8JwWD9ID0MiBrJscQBhCDEkw/A+Zc+vT/zbc//6sOvSPdgNbj7//DwLTzH0kzIHPX6//ffv8Fa77+5ud//3V4vBC5+eX/+Zc//U/d8RrMD9v08v/Tz7/Bmt99//M/dusrvGEA1gwCn37+/Z+3983/089/gPm//v77X7z/LcFABNkM1gwCf4CaYGDyuY/Ex0LOnjdgQ2Bgx72vpEQjwpDHn37/P/b0OzTQcOOBT4kUZyaKszMAV1H50t3xh/4AAAAASUVORK5CYII=', 'Technical Debt': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA5ElEQVR4AWJI3WvvmbLL9nHyLpv/pGCQHpBeBmTN5BjCAGJQgvEaACidGnQqjuLwo+U5PUB2Ux6zjSHbGhuy7bq2bfu7O+Md/zqYf/i0/N+HDcEouwZDb/XQexVwBq04ka+j+jydfoOe5wrE4lHovHK8m66gcAnQdJNPvwFZm5yZrzZ2HDTe5MEaMCCeiOPbfI+u5zLmHNRf5mBHNAVbwIRg1I/OpxL6DVI/KQRAmtFvsPDXBW/EDYnjF46gBfFEDB1PxfQbtNwW4ky1D0/YAa1Hxo4D8o8lc2QbGipw+NzDxDXOSR+lejoP7BDYAAAAAElFTkSuQmCC', }; // eslint-disable-next-line no-unused-vars const COLORS = { Bug: '#e2000c', Epic: '#9200df', Feature: '#16b307', Story: '#16b307', 'Sub-task': '#0090e6', Task: '#0090e6', }; const RELOAD_ICON = 'iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAmElEQVR4AY3SJdYCYRhA4V+BRGY5rAF3ZwvkiSyANLugYgUtOJsg4s7LxV3uOU8a+fTrom9EUMEAfZQRwDEFuwxIQ9CEetCBIIk4BF/fSKMPL26LYgHZoQgEXjxKgQCgCpqfvQwaQMVtOhhv3X7wURU08apvxGB5v2giFwTmT7bVigly+H11cAnUIChAD3p+NbrIwotv7NoAffg2NR6lsPIAAAAASUVORK5CYII='; function request(options = {}) { const OPTIONS = { hostname: BASE_URL, port: 443, path: options.path ? options.path.replace(/ /g, '%20') : '/', method: options.method || 'GET', headers: { Authorization: `Basic ${AUTH}`, }, }; return new Promise((resolve, reject) => { const req = https.request(OPTIONS, (response) => { const { headers, statusCode } = response; const isJSON = headers['content-type'].includes('application/json'); // temporary data holder const body = []; // on every content chunk, push it to the data array response.on('data', chunk => body.push(chunk)); // we are done, resolve promise with those joined chunks response.on('end', () => { const content = body.join(''); const data = isJSON ? JSON.parse(content) : content; if (statusCode < 200 || statusCode > 299) { const { errorMessages } = data; const error = errorMessages && Array.isArray(errorMessages) ? ` -- ${errorMessages[0]}` : ''; reject(new Error(`Request failed [${response.statusCode}]${error}`)); return; } resolve(data); }); }); // handle connection errors of the request req.on('error', err => reject(err)); req.end(); }); } function trimString(str, n = 48) { return (str.length > n) ? `${str.substr(0, n - 1)}…` : str; } function formatIssue(issue) { const { assignee, issuetype, parent, status, summary, timetracking, } = issue.fields; const assignment = ` (${assignee ? assignee.displayName : 'unassigned'})`; return [ `[${issue.key}] ${trimString(summary)} | href=${BROWSE_URL}${issue.key} image=${ICONS[issuetype.name]} color=#555`, parent && `▲ [${parent.key}] ${trimString(parent.fields.summary)} | href=${BROWSE_URL}${parent.key} color=gray`, `${status.name}${SHOW_ALL_ISSUES ? assignment : ''} | color=#999 font=Monaco`, SHOW_TIMETRACK && timetracking.originalEstimate ? `▶ ${timetracking.originalEstimate}` : '', ] .filter(d => !!d) .join('\n'); } const login = () => request({ path: '/rest/auth/1/session' }); const getProject = () => request({ path: `/rest/api/2/project/${PROJECT_KEY}` }); const getItems = () => { const query = [ !SHOW_ALL_ISSUES && 'assignee in(currentUser())', `status not in (${IGNORED_STATUSES.join(', ')})`, `project=${PROJECT_KEY}`, ] .filter(d => !!d) .join(' AND '); const fields = ['summary', 'issuetype', 'status', 'timetracking', 'assignee', 'parent']; return request({ path: `/rest/api/2/search?jql=${query}&fields=${fields.join(',')}&maxResults=${LIMIT}` }); }; login() .then(() => Promise.all([getProject(), getItems()])) .then(([project, items]) => { const projectUrl = `${BROWSE_URL}${project.key}`; const issues = items.issues.map(formatIssue); const content = issues.length ? issues.join('\n---\n') : 'Nothing to show | color=gray'; const output = [ `|image=${ICON}`, `${project.name} @ Jira | href=${projectUrl}`, content, `RELOAD | image=${RELOAD_ICON} refresh=true`, ]; console.log(output.join('\n---\n')); }) .catch(err => console.error(err.toString()));