├── .gitignore ├── LICENSE ├── README.md ├── commands ├── cache.js ├── review.js ├── start.js └── summary.js ├── index.js ├── lib ├── git.js ├── gitlab.js ├── jira.js └── memoize.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | !.placeholder 31 | .idea 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 packetloop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JIRA + Git flow + Gitlab Integration 2 | 3 | ## Installation 4 | 5 | Hopefully you have nodejs installed already. 6 | 7 | ```bash 8 | npm install -g packetloop/jira 9 | ``` 10 | 11 | ## Configuration: 12 | 13 | Set following ENV vars (add these to your `.bashrc` and change whatever needed) 14 | 15 | ```bash 16 | export JIRA_URL=url 17 | export JIRA_TOKEN=user:pass 18 | export GITLAB_URL=url 19 | export GITLAB_TOKEN=token 20 | export GITLAB_PING_FRONTEND="@list @of @users @to @ping @on @each @review" 21 | #export GITLAB_PING_DEFAULT="$GITLAB_PING_FRONTEND" 22 | export GITLAB_PING_DEFAULT="Hello, @everyone!" 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```bash 28 | jira start AB-1234 29 | 30 | # do something, commit, push. This will set description to $GITLAB_PING_FRONTEND 31 | jira review AB-1234 32 | 33 | # you can specify GITLAB_PING_FRONTEND group this way 34 | jira review AB-1234 frontend 35 | 36 | # to simply fetch JIRA summary 37 | jira summary AB-1234 38 | ``` 39 | 40 | There is a little dynamic contextual help on available commands and their basic syntax 41 | 42 | ```bash 43 | $ jira 44 | Usage: 45 | cache: 46 | jira cache 47 | Available actions: 48 | clear 49 | size 50 | path 51 | review: 52 | jira review [] 53 | start: 54 | jira start 55 | summary: 56 | jira summary 57 | 58 | 23:59 $ jira summary 59 | [Error: Issue key must be specified] 60 | Usage 61 | jira summary 62 | ``` 63 | 64 | ## How it works: 65 | 66 | #### `start AB-1234` 67 | 1. Make a call to jira to resolve ticket summary: `Awesome feature` 68 | 2. Dasherize it, create a new branch and switch to it: `feature/AB-1234-Awesome-feature` 69 | 70 | #### `review AB-1234` 71 | 1. Make a call to jira to resolve ticket summary: `Awesome feature` 72 | 2. Get origin url from GIT and extract project namespace `group/project` 73 | 3. Search GITLAB for project ID by namespace 74 | 4. Take your token and submit merge request `feature/AB-1234-Awesome-feature` --> `develop` 75 | 5. Ping default user group (put `GITLAB_PING_DEFAULT` to merge request description). 76 | To ping `GITLAB_PING_FRONTEND` group, use `review AB-1234 frontend` 77 | -------------------------------------------------------------------------------- /commands/cache.js: -------------------------------------------------------------------------------- 1 | var memoize = require('../lib/memoize'); 2 | 3 | var cache = function (action) { 4 | if (!action) { 5 | throw new Error('Action is required'); 6 | } 7 | 8 | if (action === 'clear') { 9 | memoize.clear(); 10 | return [ 11 | 'Cache cleared' 12 | ]; 13 | } 14 | 15 | if (action === 'size') { 16 | return [ 17 | 'Cache size: ' + memoize.size() 18 | ]; 19 | } 20 | 21 | if (action === 'path') { 22 | return [ 23 | 'Path: ' + memoize.path() 24 | ]; 25 | } 26 | 27 | throw new Error('Action ' + action + ' is not implemented'); 28 | }; 29 | 30 | cache.help = [ 31 | 'jira cache ', 32 | 'Available actions:', 33 | ' clear', 34 | ' size', 35 | ' path' 36 | ]; 37 | 38 | module.exports = cache; 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /commands/review.js: -------------------------------------------------------------------------------- 1 | var sh = require('child_process').execSync; 2 | var _ = require('lodash'); 3 | 4 | var git = require('../lib/git'); 5 | var gitlab = require('../lib/gitlab'); 6 | var memo = require('../lib/memoize'); 7 | var summary = require('./summary'); 8 | 9 | 10 | function searchProjects(namespace) { 11 | return sh(gitlab.curl('GET', '/projects', { 12 | search: namespace, page: 1, per_page: 100 13 | })).toString('utf8'); 14 | } 15 | 16 | function fetchUser() { 17 | return sh(gitlab.curl('GET', '/user', {})).toString('utf8'); 18 | } 19 | 20 | 21 | function getUserID() { 22 | 23 | var user; 24 | try { 25 | user = JSON.parse(memo(fetchUser)); 26 | } catch (e) { 27 | throw new Error('Cannot retrieve GITLAB user'); 28 | } 29 | return user.id; 30 | } 31 | 32 | 33 | function getProjectID(namespace) { 34 | 35 | var projects; 36 | try { 37 | projects = JSON.parse(memo(searchProjects, namespace)); 38 | } catch (e) { 39 | throw new Error('Cannot retrieve GITLAB projects'); 40 | } 41 | var project = _.find(projects, {path_with_namespace: namespace}); 42 | if (!project) { 43 | throw new Error('Cannot find project ' + namespace); 44 | } 45 | return project.id; 46 | } 47 | 48 | 49 | var review = function (key, group) { 50 | var results = []; 51 | 52 | if (!key) { 53 | throw new Error('Issue key must be specified'); 54 | } 55 | var namespace = git.getRepo().split(':').pop(); 56 | results.push('Project namespace is ' + namespace); 57 | 58 | var userId = getUserID(); 59 | results.push('User id resolved to ' + userId); 60 | 61 | var id = getProjectID(namespace); 62 | results.push('Project id resolved to ' + id); 63 | 64 | var name = summary(key).shift(); 65 | 66 | var branch = git.getCurrentBranch(); 67 | var autoName = git.featureBranchName([key, name].join(' ')); 68 | if (branch !== autoName) { 69 | throw new Error('Wrong branch. Expected ' + autoName + ' actual ' + branch); 70 | } 71 | 72 | var description; 73 | if (group) { 74 | description = process.env['GITLAB_PING_' + group.toUpperCase()]; 75 | } 76 | if (!description) { 77 | description = process.env['GITLAB_PING_DEFAULT']; 78 | } 79 | 80 | var mr; 81 | try { 82 | mr = sh(gitlab.curl('POST', '/projects/' + id + '/merge_requests', { 83 | id: id, 84 | assignee_id: userId, 85 | source_branch: branch, 86 | target_branch: 'develop', 87 | title: [key, name].join(' - '), 88 | description: description 89 | })).toString('utf8'); 90 | console.log("mr", mr); 91 | mr = JSON.parse(mr); 92 | } catch (e) { 93 | console.log("e", e); 94 | throw new Error('Cannot create GITLAB merge request'); 95 | } 96 | if (!mr.iid) { 97 | throw new Error('Cannot create GITLAB merge request, it may exist already'); 98 | } 99 | 100 | results.push([ 101 | process.env.GITLAB_URL.replace(/\/$/, ''), namespace, 'merge_requests', mr.iid 102 | ].join('/')); 103 | return results; 104 | }; 105 | 106 | review.help = [ 107 | 'jira review []' 108 | ].join('\n'); 109 | 110 | module.exports = review; 111 | -------------------------------------------------------------------------------- /commands/start.js: -------------------------------------------------------------------------------- 1 | var summary = require('./summary'); 2 | var git = require('../lib/git'); 3 | 4 | var start = function (key) { 5 | var name = summary(key).shift(); 6 | var result = []; 7 | var branch = git.featureBranchName([key, name].join(' ')); 8 | 9 | 10 | // create branch if does not exist 11 | if (!git.branchExists(branch)) { 12 | git.createBranch(branch); 13 | result.push('Branch `' + branch + '` created'); 14 | } 15 | git.checkoutBranch(branch); 16 | result.push('Checked out branch `' + branch + '`'); 17 | 18 | return result; 19 | }; 20 | 21 | start.help = [ 22 | 'jira start ' 23 | ]; 24 | 25 | module.exports = start; 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /commands/summary.js: -------------------------------------------------------------------------------- 1 | var sh = require('child_process').execSync; 2 | var memo = require('../lib/memoize'); 3 | var jira = require('../lib/jira'); 4 | 5 | function getIssue(key) { 6 | var cmd = jira.curl(jira.url('/issue/' + key)); 7 | return sh(cmd).toString('utf8'); 8 | } 9 | 10 | var summary = function (rawKey) { 11 | 12 | if (!rawKey) { 13 | throw new Error('Issue key must be specified'); 14 | } 15 | 16 | var key; 17 | 18 | if (String(parseInt(rawKey, 10)) === rawKey) { 19 | key = [process.env.JIRA_PREFIX, rawKey].join('-').toUpperCase() 20 | } else { 21 | key = rawKey.toUpperCase(); 22 | } 23 | 24 | var issue; 25 | 26 | try { 27 | issue = JSON.parse(memo(getIssue, key)); 28 | } catch (e) { 29 | throw new Error('Cannot retrieve JIRA issue ' + key); 30 | } 31 | if (issue.errorMessages && issue.errorMessages.length) { 32 | throw new Error(issue.errorMessages.shift()); 33 | } 34 | 35 | return [[key, issue.fields.summary.trim()].join(' ')]; 36 | }; 37 | 38 | summary.help = [ 39 | 'jira summary ' 40 | ]; 41 | 42 | module.exports = summary; 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var path = require('path'); 4 | var glob = require('glob'); 5 | var _ = require('lodash'); 6 | 7 | var args = process.argv.slice(2); 8 | var command = args.shift(); 9 | 10 | // Pre-fill all commands to populate help 11 | var commands = {}; 12 | glob.sync(path.join(__dirname, 'commands', '*.js')).forEach(function (filename) { 13 | commands[path.basename(filename, '.js')] = require(path.resolve(filename)); 14 | }); 15 | 16 | 17 | function usage() { 18 | console.log(['Usage:'].concat(_.map(commands, function(handler, name) { 19 | return [name + ':'].concat(handler.help).join('\n ') 20 | })).join('\n ')); 21 | } 22 | 23 | if (!command || command === 'help') { 24 | usage(); 25 | process.exit(0); 26 | } 27 | 28 | if (!commands[command]) { 29 | console.error('Command `' + command + '` not implemented'); 30 | usage(); 31 | process.exit(1); 32 | } 33 | 34 | try { 35 | console.log([].concat(commands[command].apply(null, args)).join('\n ')); 36 | } catch (e) { 37 | console.error(e); 38 | console.log(['Usage'].concat(commands[command].help).join('\n ')); 39 | process.exit(1); 40 | } 41 | 42 | process.exit(0); 43 | -------------------------------------------------------------------------------- /lib/git.js: -------------------------------------------------------------------------------- 1 | var sh = require('child_process').execSync; 2 | 3 | 4 | exports.getRepo = function () { 5 | var result = sh('git remote -v | grep origin | grep push | awk \'{print $2}\'').toString('utf8'); 6 | if (result.code !== 0) { 7 | throw new Error(result.trim()); 8 | } 9 | return result.trim().replace(/\.git$/, ''); 10 | }; 11 | 12 | 13 | exports.branchExists = function (branch) { 14 | return sh(['git branch', 15 | ' |', 16 | ' grep ', '"', branch, '"'].join('')).code === 0; 17 | }; 18 | 19 | 20 | exports.createBranch = function (branch) { 21 | var result = sh(['git branch ', branch].join('')); 22 | if (result.code !== 0) { 23 | throw new Error(result.trim()); 24 | } 25 | return result.toString('utf8').trim(); 26 | }; 27 | 28 | 29 | exports.checkoutBranch = function (branch) { 30 | var result = sh(['git checkout ', branch].join('')); 31 | if (result.code !== 0) { 32 | throw new Error(result.trim()); 33 | } 34 | return result.toString('utf8').trim(); 35 | }; 36 | 37 | 38 | exports.getCurrentBranch = function () { 39 | var result = sh('git branch | grep \'*\' | awk \'{print $2}\''); 40 | if (result.code !== 0) { 41 | throw new Error(result.trim()); 42 | } 43 | return result.toString('utf8').trim(); 44 | }; 45 | 46 | 47 | exports.featureBranchName = function (feature) { 48 | return [ 49 | 'feature', 50 | feature.replace(/[^a-z_0-9]+/ig, '-').replace(/^[_\-]+|[_\-]+$/ig, '') 51 | ].join('/'); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/gitlab.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | 3 | 4 | function url(path, params) { 5 | if (!process.env.GITLAB_URL) { 6 | throw new Error('GITLAB_URL env required'); 7 | } 8 | return [process.env.GITLAB_URL.replace(/\/$/, ''), '/api/v3', path] 9 | .concat(params ? ['?', querystring.stringify(params)] : []).join(''); 10 | } 11 | 12 | exports.curl = function(method, path, params) { 13 | if (!process.env.GITLAB_TOKEN) { 14 | throw new Error('GITLAB_TOKEN env required'); 15 | } 16 | var command = ['curl --silent', 17 | ' -H "PRIVATE-TOKEN: ', process.env.GITLAB_TOKEN, '"', 18 | ' -X ', method.toUpperCase(), 19 | method.toUpperCase() !== 'GET' ? ' --data "' + querystring.stringify(params) + '"' : '', 20 | ' "', method.toUpperCase() !== 'GET' ? url(path) : url(path, params), '"' 21 | ].join(''); 22 | return command; 23 | }; 24 | 25 | exports.url = url; 26 | -------------------------------------------------------------------------------- /lib/jira.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | 3 | 4 | exports.curl = function(url) { 5 | if (!process.env.JIRA_TOKEN) { 6 | throw new Error('JIRA_TOKEN env required'); 7 | } 8 | 9 | var cmd = ['curl --silent', 10 | ' -u ', process.env.JIRA_TOKEN, 11 | ' -H "Content-Type: application/json"', 12 | ' "', url, '"' 13 | ].join(''); 14 | 15 | // console.log('Executing: \n', cmd); 16 | return cmd; 17 | }; 18 | 19 | exports.url = function(path, params) { 20 | params = params || {}; 21 | 22 | if (!process.env.JIRA_URL) { 23 | throw new Error('JIRA_URL env required'); 24 | } 25 | 26 | return [process.env.JIRA_URL.replace(/\/$/, ''), '/rest/api/2', path, 27 | '?', querystring.stringify(params)].join(''); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/memoize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var os = require('os'); 4 | var fs = require('fs'); 5 | var crypto = require('crypto'); 6 | var path = require('path'); 7 | var slice = require('lodash/slice'); 8 | 9 | var db = path.join(os.tmpdir(), 'jira.json'); 10 | var cache = fs.existsSync(db) ? require(db) : {}; 11 | 12 | 13 | function persist() { 14 | return fs.writeFileSync(db, JSON.stringify(cache), 'UTF-8'); 15 | } 16 | 17 | 18 | function hash() { 19 | var hasher = crypto.createHash('sha1'); 20 | hasher.update(JSON.stringify(arguments)); 21 | return hasher.digest('hex'); 22 | } 23 | 24 | function memoize(func) { 25 | 26 | var args = slice(arguments, 1); 27 | var key = hash(func.toString(), args); 28 | if (!cache[key]) { 29 | cache[key] = func.apply(null, args); 30 | persist(); 31 | } 32 | 33 | return cache[key]; 34 | } 35 | 36 | memoize.clear = function () { 37 | cache = {}; 38 | return persist(); 39 | }; 40 | 41 | memoize.size = function () { 42 | return fs.statSync(db).size; 43 | }; 44 | 45 | memoize.path = function () { 46 | return db; 47 | }; 48 | 49 | module.exports = memoize; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@packetloop/jira", 3 | "version": "1.1.1", 4 | "publishConfig": { 5 | "registry": "https://api.bintray.com/npm/arbornetworks/packetloop" 6 | }, 7 | "preferGlobal": true, 8 | "description": "JIRA + Git flow + Gitlab Integration", 9 | "main": "index.js", 10 | "bin": { 11 | "packetloop-jira": "index.js" 12 | }, 13 | "dependencies": { 14 | "glob": "^6.0.4", 15 | "lodash": "^4.0.0" 16 | }, 17 | "devDependencies": {}, 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/packetloop/jira.git" 24 | }, 25 | "author": "Nik Butenko ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/packetloop/jira/issues" 29 | }, 30 | "homepage": "https://github.com/packetloop/jira" 31 | } 32 | --------------------------------------------------------------------------------