├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── cli.js ├── index.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true 5 | }, 6 | extends: 'eslint:recommended', 7 | parserOptions: { 8 | sourceType: 'module' 9 | }, 10 | rules: { 11 | indent: ['error', 2], 12 | 'linebreak-style': ['error', 'unix'], 13 | "no-console": 0, 14 | quotes: ['error', 'single'], 15 | semi: ['error', 'always'], 16 | 'max-len': [ 17 | 'error', 18 | 120, 19 | 2, 20 | { 21 | ignoreUrls: true, 22 | ignoreComments: true 23 | } 24 | ] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitbucket ESLint Bot 2 | 3 | Bot to run on CI server to post eslint errors on Bitbucket PRs 4 | 5 | ## Config 6 | 7 | Most configs can be passed as command line options/env vars 8 | 9 | - bitbucketUrl/BITBUCKET_URL - Base URL of bitbucket to POST to eg https://bitbucket.test.com 10 | - lintResultsPath/LINT_RESULTS_PATH - Path to JSON eslint output file 11 | - jobName/JOB_NAME - auto injected Jenkins job name - can extract repository + pullRequestID if setup correctly 12 | - password/BITBUCKET_PASSWORD - Bitbucket password for user to post comments. Be careful. 13 | - project/BITBUCKET_PROJECT - Bitbucket project name eg 'APP' 14 | - pullRequestID/PULL_REQUEST_ID - Numeric ID of pull request in Bitbucket. 15 | - repository/BITBUCKET_REPOSITORY - Bitbucket repository name eg 'test-project' 16 | - user/BITBUCKET_USER - Bitbucket user to post comments eg 'tabrindle' 17 | - commentFileLevel - Write comments on each file at line of violation. Defaults to true. 18 | - commentTopLevel - Write a comment on the top level of the PR. Defaults to true 19 | - warnings - write comments for warnings. Defaults to true. 20 | - createTask - create a task for top level comment. Defaults to false. 21 | - debug - Print console statements before POSTs 22 | 23 | ## Alternative usage 24 | Can also be used as a js module. 25 | 26 | ``` 27 | require('bitbucket-eslint-bot').run({ 28 | bitbucketUrl: 'https://code.company.com', 29 | pullRequestID: process.env.BRANCH_NAME, 30 | commentFileLevel: false, 31 | createTask: true, 32 | lintResultsPath: './eslint-results.json', 33 | password: process.env.GIT_PASSWORD, 34 | project: 'BTBKT', 35 | repository: 'client', 36 | user: process.env.GIT_USERNAME, 37 | warnings: false, 38 | }); 39 | 40 | ``` 41 | 42 | ## Example 43 | 44 | - File level comment 45 | [eslint] 'iconNames' is never reassigned. Use 'const' instead. - ([prefer-const](https://github.com/eslint/eslint/blob/master/docs/rules/prefer-const.md)) 46 | 47 | - Top level comment 48 | [eslint] This PR contains 1 lint error 49 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var bot = require('./index.js'); 4 | var argv = require('minimist')(process.argv.slice(2)); 5 | 6 | bot.run(argv); 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const path = require("path"); 3 | 4 | const eslintDocsBaseUrl = 5 | "https://github.com/eslint/eslint/blob/master/docs/rules"; 6 | 7 | const encodeAuthorization = ({ user, password }) => { 8 | return `Basic ${new Buffer(`${user}:${password}`, "binary").toString( 9 | "base64" 10 | )}=`; 11 | }; 12 | 13 | const requiredParams = (params = {}) => { 14 | const missing = Object.keys(params) 15 | .filter(filterKey => params[filterKey] === undefined) // eslint-disable-line no-undefined 16 | .reduce((acc, reduceKey) => `${acc} ${reduceKey}`, ""); 17 | console.log(missing); 18 | if (missing) { 19 | throw new TypeError( 20 | `The following required parameters are missing:${missing}` 21 | ); 22 | } else { 23 | return true; 24 | } 25 | }; 26 | 27 | const commentTopLevelFn = function(lintResults, url, bitbucketUrl, options) { 28 | const errors = lintResults.reduce((sum, file) => sum + file.errorCount, 0); 29 | const errorPlural = errors === 1 ? "error" : "errors"; 30 | 31 | if (options.debug) { 32 | console.log("POST to: ", url); 33 | console.log(`[eslint] This PR contains ${errors} lint ${errorPlural}`); 34 | return; 35 | } 36 | 37 | return fetch(url, { 38 | method: "POST", 39 | body: JSON.stringify({ 40 | text: `[eslint] This PR contains ${errors} lint ${errorPlural}` 41 | }), 42 | credentials: "include", 43 | headers: { 44 | Authorization: encodeAuthorization(options), 45 | "Content-Type": "application/json" 46 | } 47 | }) 48 | .then(r => { 49 | if (r.ok) { 50 | return r.json(); 51 | } else { 52 | throw { url: r.url, status: r.status, statusText: r.statusText }; 53 | } 54 | }) 55 | .then(response => { 56 | if (options.createTask && errors > 0) 57 | return fetch(`${bitbucketUrl}/rest/api/1.0/tasks`, { 58 | method: "POST", 59 | body: JSON.stringify({ 60 | text: `fix ${errors} lint ${errorPlural}`, 61 | anchor: { 62 | id: response.id, 63 | type: "COMMENT" 64 | } 65 | }), 66 | credentials: "include", 67 | headers: { 68 | Authorization: encodeAuthorization(options), 69 | "Content-Type": "application/json" 70 | } 71 | }).then(r => { 72 | if (r.ok) { 73 | return r.json(); 74 | } else { 75 | throw { url: r.url, status: r.status, statusText: r.statusText }; 76 | } 77 | }); 78 | }) 79 | .catch(console.log); 80 | }; 81 | 82 | const commentFileLevelFn = function(lintResults, url, options) { 83 | return lintResults.map(file => { 84 | if (file.messages.length) { 85 | return file.messages 86 | .filter(message => { 87 | return !(!options.warnings && message.severity === 1); 88 | }) 89 | .map(message => { 90 | if (options.debug) { 91 | console.log("POST to: ", url); 92 | console.log( 93 | `[eslint] ${message.message} - ([${message.ruleId}](${eslintDocsBaseUrl}/${message.ruleId}.md))` 94 | ); 95 | console.log({ 96 | line: message.line, 97 | lineType: "ADDED", 98 | fileType: "TO", 99 | path: file.filePath.split(process.cwd())[1] 100 | }); 101 | return; 102 | } 103 | 104 | fetch(url, { 105 | method: "POST", 106 | body: JSON.stringify({ 107 | text: `[eslint] ${message.message} - ([${message.ruleId}](${eslintDocsBaseUrl}/${message.ruleId}.md))`, 108 | anchor: { 109 | line: message.line, 110 | lineType: "ADDED", 111 | fileType: "TO", 112 | path: file.filePath.split(process.cwd())[1] 113 | } 114 | }), 115 | credentials: "include", 116 | headers: { 117 | Authorization: encodeAuthorization(options), 118 | "Content-Type": "application/json" 119 | } 120 | }).catch(console.log); 121 | }); 122 | } 123 | }); 124 | }; 125 | 126 | module.exports.run = function({ 127 | bitbucketUrl = process.env.BITBUCKET_URL, 128 | commentFileLevel = true, 129 | commentTopLevel = true, 130 | createTask = true, 131 | debug = false, 132 | warnings = true, 133 | lintResultsPath = process.env.LINT_RESULTS_PATH, 134 | jobName = process.env.JOB_NAME, 135 | password = process.env.BITBUCKET_PASSWORD, 136 | project = process.env.BITBUCKET_PROJECT, 137 | pullRequestID = process.env.PULL_REQUEST_ID, 138 | repository = process.env.BITBUCKET_REPOSITORY, 139 | user = process.env.BITBUCKET_USER 140 | } = {}) { 141 | if (jobName) { 142 | repository = repository || jobName.split("/")[0]; 143 | if (!/PR-(\d*)/.test(jobName)) { 144 | console.log("Job is not a PR, and there is no supplied pullRequestID"); 145 | return; 146 | } 147 | pullRequestID = pullRequestID || jobName.split("/")[1].match(/PR-(\d*)/)[1]; 148 | } 149 | 150 | try { 151 | requiredParams({ 152 | bitbucketUrl, 153 | lintResultsPath, 154 | password, 155 | project, 156 | pullRequestID, 157 | repository, 158 | user 159 | }); 160 | 161 | const lintResults = require(path.resolve(process.cwd(), lintResultsPath)); 162 | const url = `${bitbucketUrl}/rest/api/1.0/projects/${project}/repos/${repository}/pull-requests/${pullRequestID}/comments`; 163 | 164 | if (lintResults && lintResults.length) { 165 | if (commentTopLevel) { 166 | commentTopLevelFn(lintResults, url, bitbucketUrl, { 167 | debug, 168 | user, 169 | password, 170 | createTask 171 | }); 172 | } 173 | if (commentFileLevel) { 174 | commentFileLevelFn(lintResults, url, { 175 | debug, 176 | user, 177 | password, 178 | warnings 179 | }); 180 | } 181 | } 182 | } catch (err) { 183 | throw err; 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitbucket-eslint-bot", 3 | "version": "1.1.2", 4 | "description": "Bot to run on CI to post eslint errors on Bitbucket PRs", 5 | "main": "index.js", 6 | "bin": { 7 | "eslint-bot": "cli.js" 8 | }, 9 | "keywords": [ 10 | "Bitbucket", 11 | "CI", 12 | "eslint", 13 | "bot", 14 | "jenkins" 15 | ], 16 | "author": "tabrindle@gmail.com", 17 | "license": "MIT", 18 | "scripts": { 19 | "lint": "eslint index.js", 20 | "format": "prettier-eslint index.js --write" 21 | }, 22 | "dependencies": { 23 | "minimist": "^1.2.0", 24 | "node-fetch": "^1.7.3" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^4.6.1", 28 | "prettier-eslint-cli": "^4.1.1" 29 | } 30 | } 31 | --------------------------------------------------------------------------------