├── .npmignore ├── src ├── system.js ├── utils.js └── client.js ├── presets ├── priority.json ├── default.json └── progress.json ├── .jshintrc ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── bin └── github-label.js /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | tests 3 | support 4 | testing 5 | docs 6 | example 7 | examples 8 | node_modules 9 | gulpfile.js 10 | Gulpfile.js 11 | Gruntfile.js 12 | README.md 13 | .jshintrc 14 | -------------------------------------------------------------------------------- /src/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.exit = function(message) { 4 | if (message) { 5 | console.log('Exit: ' + message); 6 | } 7 | process.exit(1); 8 | }; 9 | 10 | exports.success = function(message) { 11 | if (message) { 12 | console.log('Success: ' + message); 13 | } 14 | process.exit(0); 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var system = require('./system'); 5 | 6 | exports.readJSON = function(filepath, callback) { 7 | fs.readFile(filepath, 'utf8', function(error, data) { 8 | if (error) { 9 | system.exit('could not read file: ' + filepath); 10 | } callback(JSON.parse(data)); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /presets/priority.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "priority 1: urgent", 4 | "color": "D62929" 5 | }, 6 | { 7 | "name": "priority 2: high", 8 | "color": "FFA04D" 9 | }, 10 | { 11 | "name": "priority 3: medium", 12 | "color": "F7F697" 13 | }, 14 | { 15 | "name": "priority 4: low", 16 | "color": "C8EDBE" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "mocha": true 21 | } 22 | -------------------------------------------------------------------------------- /presets/default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "bug", 4 | "color": "fc2929" 5 | }, 6 | { 7 | "name": "duplicate", 8 | "color": "cccccc" 9 | }, 10 | { 11 | "name": "enhancement", 12 | "color": "84b6eb" 13 | }, 14 | { 15 | "name": "help wanted", 16 | "color": "159818" 17 | }, 18 | { 19 | "name": "invalid", 20 | "color": "e6e6e6" 21 | }, 22 | { 23 | "name": "question", 24 | "color": "cc317c" 25 | }, 26 | { 27 | "name": "wontfix", 28 | "color": "ffffff" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /.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 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /presets/progress.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "progress: backlog", 4 | "color": "EBEBEB" 5 | }, 6 | { 7 | "name": "progress: blocked", 8 | "color": "FC4242" 9 | }, 10 | { 11 | "name": "progress: standby", 12 | "color": "D0F2F0" 13 | }, 14 | { 15 | "name": "progress: release", 16 | "color": "C5EBA0" 17 | }, 18 | { 19 | "name": "progress: design", 20 | "color": "695ABB" 21 | }, 22 | { 23 | "name": "progress: development", 24 | "color": "2D8EFA" 25 | }, 26 | { 27 | "name": "progress: testing", 28 | "color": "F29141" 29 | }, 30 | { 31 | "name": "progress: review", 32 | "color": "FFFCAB" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-label", 3 | "version": "1.2.1", 4 | "description": "CLI for creating GitHub labels", 5 | "bin": { 6 | "github-label": "bin/github-label.js" 7 | }, 8 | "main": "src/client.js", 9 | "preferGlobal": "true", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/codenameyau/github-label.git" 16 | }, 17 | "keywords": [ 18 | "github", 19 | "labels", 20 | "label", 21 | "issues" 22 | ], 23 | "author": "Jorge Yau ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/codenameyau/github-label/issues" 27 | }, 28 | "homepage": "https://github.com/codenameyau/github-label", 29 | "dependencies": { 30 | "async": "^1.2.1", 31 | "commander": "^2.8.1", 32 | "octonode": "^0.6.17", 33 | "prompt": "^0.2.14", 34 | "require-dir": "^0.3.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jorge Yau 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 | # github-label 2 | 3 | [![NPM version](http://img.shields.io/npm/v/github-label.svg)](https://www.npmjs.org/package/github-label) 4 | [![license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/codenameyau/github-label/blob/master/LICENSE) 5 | 6 | Node command-line tool used to create or remove GitHub labels. 7 | Predefined labels are available in the [presets folder](https://github.com/codenameyau/github-label/tree/master/presets). 8 | You can also create and use your own labels with JSON. 9 | 10 | ## Installation and Setup 11 | ``` 12 | npm install -g github-label 13 | ``` 14 | 15 | ### Authentication with GitHub Access Token 16 | Use this method if don't want to type your username and password. 17 | 18 | Create a [Personal access token](https://github.com/settings/tokens) on GitHub 19 | with the `repo` and `public_repo` permissions enabled. Then add the following 20 | environment variable in your `.bashrc` (Linux) or `.bash_profile` (Mac). 21 | 22 | ```bash 23 | export GITHUB_LABEL_TOKEN='REPLACE THIS WITH YOUR TOKEN' 24 | ``` 25 | 26 | ## Usage Examples 27 | 28 | ``` 29 | Usage: github-label [options] 30 | 31 | Options: 32 | 33 | -h, --help output usage information 34 | -V, --version output the version number 35 | -p, --preset [value] Specify a label preset. 36 | -l, --list [value] List the default preset. 37 | -j, --json [value] Specify your own JSON label preset. 38 | -s, --skip Skip existing labels instead of updating them. 39 | -r, --remove Remove a GitHub label preset. 40 | -R, --remove-all Removes all labels. 41 | ``` 42 | 43 | -- 44 | 45 | ### Terminal 46 | ```bash 47 | # Output the labels for the repository. 48 | github-label 'codenameyau/github-label' 49 | 50 | # List the available label presets. 51 | github-label -l 52 | 53 | # List the labels for a given preset. 54 | github-label -l priority 55 | 56 | # Create labels with one of the available presets. 57 | github-label 'codenameyau/github-label' -p priority 58 | 59 | # Create labels by specifying your own JSON. 60 | github-label 'codenameyau/github-label' -j 'path-to/preset.json' 61 | 62 | # Delete all labels from a given preset or json file. 63 | github-label 'codenameyau/github-label' -p priority -r 64 | github-label 'codenameyau/github-label' -j 'path-to/preset.json' -r 65 | 66 | # Remove all labels. 67 | github-label 'codenameyau/github-label' -R 68 | ``` 69 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var github = require('octonode'); 4 | 5 | 6 | /******************************************************************** 7 | * FUNCTION CONSTRUCTOR 8 | *********************************************************************/ 9 | function GithubClient() { 10 | // Only true if environment variable exists. 11 | this.ACCESS_TOKEN = process.env.GITHUB_LABEL_TOKEN; 12 | this.repository = ''; 13 | this.client = null; 14 | } 15 | 16 | 17 | /******************************************************************** 18 | * PUBLIC METHODS 19 | *********************************************************************/ 20 | GithubClient.prototype.setRepository = function(repository) { 21 | this.repository = repository; 22 | }; 23 | 24 | GithubClient.prototype.getRepository = function() { 25 | return this.repository; 26 | }; 27 | 28 | GithubClient.prototype.setupTokenClient = function() { 29 | this.client = github.client(this.ACCESS_TOKEN); 30 | }; 31 | 32 | GithubClient.prototype.setupAuthClient = function(username, password) { 33 | this.client = github.client({ 34 | username: username, 35 | password: password 36 | }); 37 | }; 38 | 39 | GithubClient.prototype.getLabels = function(callback) { 40 | var ghrepo = this.client.repo(this.repository); 41 | ghrepo.labels(function(error, data, header) { 42 | callback(error, data, header); 43 | }); 44 | }; 45 | 46 | GithubClient.prototype.getLabel = function(label, callback) { 47 | var ghlabel = this.client.label(this.repository, label.name); 48 | ghlabel.info(function(error, data, header) { 49 | callback(error, data, header); 50 | }) 51 | }; 52 | 53 | GithubClient.prototype.createLabel = function(label, callback) { 54 | var ghrepo = this.client.repo(this.repository); 55 | ghrepo.label(label, function(error, data, header) { 56 | callback(error, data, header); 57 | }); 58 | }; 59 | 60 | GithubClient.prototype.updateLabel = function(label, callback) { 61 | var ghlabel = this.client.label(this.repository, label.name); 62 | ghlabel.update({color: label.color}, function(error, data, header) { 63 | callback(error, data, header); 64 | }) 65 | }; 66 | 67 | GithubClient.prototype.removeLabel = function(label, callback) { 68 | var ghlabel = this.client.label(this.repository, label.name); 69 | ghlabel.delete(function(error, data, header) { 70 | callback(error, data, header); 71 | }); 72 | }; 73 | 74 | 75 | /******************************************************************** 76 | * MODULE EXPORT 77 | *********************************************************************/ 78 | module.exports = GithubClient; 79 | -------------------------------------------------------------------------------- /bin/github-label.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*! 3 | * github-label 4 | * MIT License (c) 2015 5 | * https://github.com/codenameyau/github-label 6 | */ 7 | 'use strict'; 8 | 9 | var program = require('commander'); 10 | var prompt = require('prompt'); 11 | var async = require('async'); 12 | var requireDir = require('require-dir'); 13 | var format = require('util').format; 14 | var GithubClient = require('../src/client'); 15 | var system = require('../src/system'); 16 | var utils = require('../src/utils'); 17 | var pjson = require('../package.json'); 18 | var presets = requireDir('../presets'); 19 | 20 | 21 | /******************************************************************** 22 | * HELPER FUNCTIONS 23 | *********************************************************************/ 24 | var OUTPUT = { 25 | created: '[+] Created label: "%s"', 26 | updated: '[+] Updated label: "%s"', 27 | skipped: '[-] Skipped label: "%s"', 28 | removed: '[x] Removed label: "%s"', 29 | }; 30 | 31 | var hasInvalidOptions = function(program) { 32 | return program.rawArgs.length < 3 || [ 33 | program.preset, 34 | program.json 35 | ].some(function(option) { 36 | return option === true; 37 | }); 38 | }; 39 | 40 | var promptForCredentials = function(client, callback) { 41 | console.log('Please enter your GitHub credentials.\n'); 42 | var authPrompt = [ 43 | { name: 'username', type: 'string', required: true }, 44 | { name: 'password', type: 'string', required: true, hidden: true }]; 45 | prompt.start(); 46 | prompt.get(authPrompt, function(error, result) { 47 | if (error) { 48 | system.exit('invalid login credentials.'); 49 | } client.setupAuthClient(result.username, result.password); 50 | callback(); 51 | }); 52 | }; 53 | 54 | var exitOn404 = function(error) { 55 | if (error && error.statusCode === 404) { 56 | system.exit('repository does not exist.'); 57 | } 58 | }; 59 | 60 | var showPresets = function(presetName) { 61 | if (typeof(presetName) === 'string') { 62 | // Print the labels in the preset. 63 | console.log('Preset: %s\n', presetName); 64 | var labels = presets[presetName]; 65 | labels.forEach(function(value) { 66 | console.log('#%s - %s', value.color, value.name); 67 | }); 68 | } else if (presetName === true) { 69 | // Only print the preset names. 70 | console.log('List of available presets:\n'); 71 | for (var label in presets) { 72 | if (presets.hasOwnProperty(label)) { 73 | console.log(label); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | var getLabels = function(client, callback) { 80 | client.getLabels(function(error, data) { 81 | exitOn404(error); 82 | callback(data); 83 | }); 84 | }; 85 | 86 | var outputLabels = function(client) { 87 | getLabels(client, function(data) { 88 | data.forEach(function(element) { 89 | console.log('#%s - %s', element.color, element.name); 90 | }); 91 | }); 92 | }; 93 | 94 | var createLabels = function(client, program) { 95 | async.each(program.labels, function(label) { 96 | client.createLabel(label, function(error, data) { 97 | exitOn404(error); 98 | // Status code 422 indicates that the label already exists. 99 | if (error && error.statusCode === 422) { 100 | console.log(OUTPUT.skipped, label.name); 101 | } else if (data) { 102 | console.log(OUTPUT.created, data.name); 103 | } 104 | }); 105 | }); 106 | }; 107 | 108 | var createOrUpdateLabels = function(client, program) { 109 | async.each(program.labels, function(label) { 110 | client.getLabel(label, function(error, data) { 111 | 112 | // Create the label if it does not exist. 113 | if (error && error.statusCode === 404) { 114 | client.createLabel(label, function(error, data) { 115 | if (error) { 116 | console.log(error); 117 | } else { 118 | console.log(OUTPUT.created, label.name); 119 | } 120 | }); 121 | } 122 | 123 | // Update the label color if different. 124 | else if (data.color !== label.color) { 125 | client.updateLabel(label, function(error, data) { 126 | if (error) { 127 | console.log(error); 128 | } else { 129 | console.log(OUTPUT.updated, label.name); 130 | } 131 | }) 132 | } 133 | 134 | // Otherwise skip the label. 135 | else { 136 | console.log(OUTPUT.skipped, label.name); 137 | } 138 | }); 139 | }); 140 | }; 141 | 142 | var removeLabels = function(client, program) { 143 | async.each(program.labels, function(label) { 144 | client.removeLabel(label, function(error) { 145 | if (error && error.statusCode === 404) { 146 | console.log(OUTPUT.skipped, label.name); 147 | } else { 148 | console.log(OUTPUT.removed, label.name); 149 | } 150 | }); 151 | }); 152 | }; 153 | 154 | var removeAllLabels = function(client) { 155 | getLabels(client, function(data) { 156 | removeLabels(client, data); 157 | }); 158 | }; 159 | 160 | var sendClientRequest = function(repository, program, callback) { 161 | var client = new GithubClient(); 162 | client.setRepository(repository); 163 | 164 | // Authenticate client with access token. 165 | if (client.ACCESS_TOKEN) { 166 | client.setupTokenClient(); 167 | callback(client, program); 168 | } 169 | 170 | // Authenticate client with credentials. 171 | else { 172 | promptForCredentials(client, function() { 173 | callback(client, program); 174 | }); 175 | } 176 | }; 177 | 178 | 179 | /******************************************************************** 180 | * MAIN CLI PROGRAM 181 | *********************************************************************/ 182 | program.version(pjson.version) 183 | .arguments('repo') 184 | .option('-p, --preset [value]', 'Specify a label preset.') 185 | .option('-l, --list [value]', 'List the default preset.') 186 | .option('-j, --json [value]', 'Specify your own JSON label preset.') 187 | .option('-s, --skip', 'Skip existing labels instead of updating them.') 188 | .option('-r, --remove', 'Remove a GitHub label preset.') 189 | .option('-R, --remove-all', 'Removes all labels.') 190 | .parse(process.argv); 191 | 192 | // Show help if no arguments are provided. 193 | if (hasInvalidOptions(program)) { 194 | program.help(); 195 | } 196 | 197 | // List the default presets. 198 | if (program.list) { 199 | showPresets(program.list); 200 | system.success(); 201 | } 202 | 203 | // Validate that a repository is specified. 204 | var repository = program.args[0]; 205 | if (!repository) { 206 | system.exit('please specify a repository like "user/repo"'); 207 | } 208 | 209 | // Remove all labels. 210 | if (program.removeAll) { 211 | sendClientRequest(repository, program, removeAllLabels); 212 | } 213 | 214 | // Read JSON file if specfied by user. 215 | else if (program.json) { 216 | utils.readJSON(program.json, function(data) { 217 | program.labels = data; 218 | if (program.remove) { 219 | sendClientRequest(repository, program, removeLabels); 220 | } else if (program.skip) { 221 | sendClientRequest(repository, program, createLabels); 222 | } else { 223 | sendClientRequest(repository, program, createOrUpdateLabels); 224 | } 225 | }); 226 | } 227 | 228 | // Use one of the specified label preset. 229 | else if (program.preset) { 230 | var labels = presets[program.preset]; 231 | program.labels = labels; 232 | if (!labels) { 233 | system.exit(format('preset "%s" doesn\'t exist.', program.preset)); 234 | } 235 | if (program.remove) { 236 | sendClientRequest(repository, program, removeLabels); 237 | } else if (program.skip) { 238 | sendClientRequest(repository, program, createLabels); 239 | } else { 240 | sendClientRequest(repository, program, createOrUpdateLabels); 241 | } 242 | } 243 | 244 | // Output existing repository labels. 245 | else { 246 | sendClientRequest(repository, program, outputLabels); 247 | } 248 | --------------------------------------------------------------------------------