diff --git a/.eslintrc b/.eslintrc index 6521401..3e07b83 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,11 +3,17 @@ "node": true }, "parserOptions": { - "ecmaVersion": 8 + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } }, "extends": "eslint-config-topcoder/nodejs", "rules": { - "max-len": ["error", 160], + "max-len": [ + "error", + 160 + ], "indent": [ "error", 2 @@ -25,12 +31,16 @@ "always" ], "no-console": 0, - "comma-dangle": ["error", { - "arrays": "never", - "objects": "never", - "imports": "never", - "exports": "never", - "functions": "ignore" - }] + "comma-dangle": [ + "error", + { + "arrays": "never", + "objects": "never", + "imports": "never", + "exports": "never", + "functions": "ignore" + } + ], + "max-lines": 0, } -} +} \ No newline at end of file diff --git a/config/default.js b/config/default.js index 1177457..6f7556e 100644 --- a/config/default.js +++ b/config/default.js @@ -82,5 +82,8 @@ module.exports = { GITLAB_API_BASE_URL: process.env.GITLAB_API_BASE_URL || 'https://gitlab.com', PAID_ISSUE_LABEL: process.env.PAID_ISSUE_LABEL || 'Paid', FIX_ACCEPTED_ISSUE_LABEL: process.env.FIX_ACCEPTED_ISSUE_LABEL || 'Fix accepted', - TC_OR_DETAIL_LINK: process.env.TC_OR_DETAIL_LINK || 'https://software.topcoder-dev.com/review/actions/ViewProjectDetails?pid=' + READY_FOR_REVIEW_ISSUE_LABEL: process.env.READY_FOR_REVIEW_ISSUE_LABEL || 'Ready for review', + TC_OR_DETAIL_LINK: process.env.TC_OR_DETAIL_LINK || 'https://software.topcoder-dev.com/review/actions/ViewProjectDetails?pid=', + RETRY_COUNT: process.env.RETRY_COUNT || 3, + RETRY_INTERVAL: process.env.RETRY_INTERVAL || 120000, // 2 minutes }; diff --git a/configuration.md b/configuration.md index 50c3ef8..ac148a8 100644 --- a/configuration.md +++ b/configuration.md @@ -26,6 +26,9 @@ The following config parameters are supported, they are defined in `config/defau |PAID_ISSUE_LABEL|the label name for paid, should be one of the label configured in topcoder x ui|'Paid'| |FIX_ACCEPTED_ISSUE_LABEL|the label name for fix accepted, should be one of the label configured in topcoder x ui|'Fix Accepted'| |TC_OR_DETAIL_LINK|the link to online review detail of challenge| see `default.js`, OR link for dev environment| +|RETRY_COUNT| the number of times an event should be retried to process| 3| +|RETRY_INTERVAL| the interval at which the event should be retried to process in milliseconds | 120000| +|READY_FOR_REVIEW_ISSUE_LABEL| the label name for ready for review, should be one of the label configured in topcoder x ui|'Ready for review'| KAFKA_OPTIONS should be object as described in https://github.com/SOHU-Co/kafka-node#kafkaclient For using with SSL, the options should be as diff --git a/constants.js b/constants.js index 198ad87..a2d9d3c 100644 --- a/constants.js +++ b/constants.js @@ -22,7 +22,10 @@ const USER_ROLES = { OWNER: 'owner' }; +const SERVICE_ERROR_STATUS = 500; + module.exports = { USER_ROLES, - USER_TYPES + USER_TYPES, + SERVICE_ERROR_STATUS }; diff --git a/package-lock.json b/package-lock.json index 0cf582c..e95f2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, "acorn": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", @@ -67,7 +61,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "ansi-styles": { "version": "2.2.1", @@ -75,22 +70,6 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", @@ -147,11 +126,18 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "async": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "requires": { - "lodash": "^4.14.0" + "lodash": "^4.17.10" + }, + "dependencies": { + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + } } }, "asynckit": { @@ -219,20 +205,41 @@ "optional": true }, "bl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", - "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "inherits": "~2.0.0" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "bluebird": { @@ -361,7 +368,8 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true }, "colors": { "version": "1.0.3", @@ -406,11 +414,6 @@ "os-homedir": "1.0.2" } }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -522,12 +525,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true - }, "doctrine": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", @@ -1037,18 +1034,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "function-bind": { "version": "1.1.1", @@ -1056,22 +1043,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -1087,6 +1058,11 @@ "is-property": "^1.0.0" } }, + "get-parameter-names": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/get-parameter-names/-/get-parameter-names-0.3.0.tgz", + "integrity": "sha1-LSI3zVkubFuFmrLv2rQ18Ajlu5c=" + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1119,6 +1095,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1151,7 +1128,8 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true }, "har-schema": { "version": "2.0.0", @@ -1185,12 +1163,6 @@ "ansi-regex": "^2.0.0" } }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true - }, "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", @@ -1259,6 +1231,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -1332,6 +1305,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1433,12 +1407,6 @@ } } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "optional": true - }, "isomorphic-fetch": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", @@ -1558,9 +1526,9 @@ "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" }, "kafka-node": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/kafka-node/-/kafka-node-2.2.3.tgz", - "integrity": "sha1-kAvXjPcAaxxcxBD2Bf77RislgSA=", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/kafka-node/-/kafka-node-2.6.1.tgz", + "integrity": "sha512-tpivkSLjiGHRLwx0YN87fMUATOK4NYWESJneHlpikEBNNA5od7fW/ikovS3tWooMqG4Nri55vPFRUNiNvNBWZA==", "requires": { "async": "^2.5.0", "binary": "~0.3.0", @@ -1707,12 +1675,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, "requires": { "minimist": "0.0.8" } @@ -1839,9 +1809,9 @@ "dev": true }, "nan": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "optional": true }, "natural-compare": { @@ -1851,12 +1821,9 @@ "dev": true }, "nested-error-stacks": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.0.tgz", - "integrity": "sha1-mLL/rvtGEPo5NvHnFDXTBwDeKEA=", - "requires": { - "inherits": "~2.0.1" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, "netrc": { "version": "0.1.4", @@ -1885,27 +1852,6 @@ "url-join": "^4.0.0" } }, - "node-gyp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", - "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "2", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - } - }, "node-zookeeper-client": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/node-zookeeper-client/-/node-zookeeper-client-0.2.2.tgz", @@ -1927,15 +1873,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.4.0.tgz", "integrity": "sha512-d6E/wK/0Ijoh5v9si5nnkDTIPfz/1sCvwpIdqwoLscJtlNEp0ggysQxffu7tS/Bpv1bMNxl6cjgD3sL4UcYRrQ==" }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "optional": true, - "requires": { - "abbrev": "1" - } - }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -1948,22 +1885,11 @@ "validate-npm-package-license": "^3.0.1" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "oauth-sign": { "version": "0.8.2", @@ -1973,7 +1899,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true }, "object-keys": { "version": "1.0.11", @@ -1985,6 +1912,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -2019,22 +1947,6 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-limit": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", @@ -2079,7 +1991,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-is-inside": { "version": "1.0.2", @@ -2373,6 +2286,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, "requires": { "glob": "^7.0.5" } @@ -2402,12 +2316,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "optional": true - }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -2425,12 +2333,6 @@ "rechoir": "^0.6.2" } }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "optional": true - }, "slice-ansi": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", @@ -2443,14 +2345,13 @@ "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, "snappy": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/snappy/-/snappy-6.0.1.tgz", - "integrity": "sha512-wrbLPjpDgDOA/VTQk/okf/qRhnWLueejiiZYMhvM9zK8NzPyLD14hIoItXya4q76u58OuUGduANks6DS8jOaJg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-6.0.4.tgz", + "integrity": "sha512-+MjETxi/G7fLtiLFWW9n9VLzpJvOVqRRohJ7kTgaU4bUJ37rsoWwxhZzO91BOB7sCgOILtKsGtCUviUo3REIfQ==", "optional": true, "requires": { "bindings": "^1.3.0", - "nan": "^2.6.1", - "node-gyp": "^3.6.2" + "nan": "^2.10.0" } }, "sntp": { @@ -2516,6 +2417,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2539,6 +2441,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2710,17 +2613,6 @@ } } }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "optional": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", @@ -2948,6 +2840,14 @@ "os-homedir": "^1.0.0" } }, + "util": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz", + "integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==", + "requires": { + "inherits": "2.0.3" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2984,24 +2884,6 @@ "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=", "dev": true }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, "winston": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", @@ -3031,7 +2913,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write": { "version": "0.2.1", diff --git a/package.json b/package.json index 2baa698..7eee338 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,11 @@ "dependencies": { "axios": "^0.16.2", "config": "^1.26.2", + "get-parameter-names": "^0.3.0", "github": "^12.0.2", "joi": "^13.0.0", "jwt-decode": "^2.2.0", - "kafka-node": "^2.2.3", + "kafka-node": "^2.6.1", "lodash": "^4.17.4", "markdown-it": "^8.4.0", "moment": "^2.19.1", @@ -37,6 +38,7 @@ "topcoder-api-projects": "^1.0.1", "topcoder-dev-api-challenges": "^1.0.6", "topcoder-dev-api-projects": "^1.0.1", + "util": "^0.11.0", "winston": "^2.3.1" }, "devDependencies": { diff --git a/services/EmailService.js b/services/EmailService.js index f907b33..80ca146 100644 --- a/services/EmailService.js +++ b/services/EmailService.js @@ -62,3 +62,5 @@ sendNewBidEmail.schema = { module.exports = { sendNewBidEmail }; + +logger.buildService(module.exports); diff --git a/services/GithubService.js b/services/GithubService.js index 8c11870..cfef8a2 100644 --- a/services/GithubService.js +++ b/services/GithubService.js @@ -14,6 +14,7 @@ const Joi = require('joi'); const GitHubApi = require('github'); const config = require('config'); const logger = require('../utils/logger'); +const errors = require('../utils/errors'); const copilotUserSchema = Joi.object().keys({ accessToken: Joi.string().required(), @@ -28,12 +29,16 @@ const copilotUserSchema = Joi.object().keys({ * @private */ async function _authenticate(accessToken) { - const github = new GitHubApi(); - github.authenticate({ - type: 'oauth', - token: accessToken - }); - return github; + try { + const github = new GitHubApi(); + github.authenticate({ + type: 'oauth', + token: accessToken + }); + return github; + } catch (err) { + throw errors.convertGitHubError(err, 'Failed to authenticate to Github using access token of copilot.'); + } } /** @@ -46,14 +51,18 @@ async function _authenticate(accessToken) { * @private */ async function _removeAssignees(github, owner, repo, number, assignees) { - await github.issues.removeAssigneesFromIssue({ - owner, - repo, - number, - body: { - assignees - } - }); + try { + await github.issues.removeAssigneesFromIssue({ + owner, + repo, + number, + body: { + assignees + } + }); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during remove assignees from issue.'); + } } /** @@ -78,7 +87,11 @@ async function updateIssue(copilot, repo, number, title) { Joi.attempt({copilot, repo, number, title}, updateIssue.schema); const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); - await github.issues.edit({owner, repo, number, title}); + try { + await github.issues.edit({owner, repo, number, title}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during updating issue.'); + } logger.debug(`Github issue title is updated for issue number ${number}`); } @@ -100,13 +113,17 @@ async function assignUser(copilot, repo, number, user) { Joi.attempt({copilot, repo, number, user}, assignUser.schema); const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); + try { + const issue = await github.issues.get({owner, repo, number}); - const issue = await github.issues.get({owner, repo, number}); - const oldAssignees = _(issue.data.assignees).map('login').without(user).value(); - if (oldAssignees && oldAssignees.length > 0) { - await _removeAssignees(github, owner, repo, number, oldAssignees); + const oldAssignees = _(issue.data.assignees).map('login').without(user).value(); + if (oldAssignees && oldAssignees.length > 0) { + await _removeAssignees(github, owner, repo, number, oldAssignees); + } + await github.issues.addAssigneesToIssue({owner, repo, number, assignees: [user]}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during assigning issue user.'); } - await github.issues.addAssigneesToIssue({owner, repo, number, assignees: [user]}); logger.debug(`Github issue with number ${number} is assigned to ${user}`); } @@ -129,7 +146,6 @@ async function removeAssign(copilot, repo, number, user) { const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); - await _removeAssignees(github, owner, repo, number, [user]); logger.debug(`Github user ${user} is unassigned from issue number ${number}`); } @@ -148,7 +164,11 @@ async function createComment(copilot, repo, number, body) { const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); - await github.issues.createComment({owner, repo, number, body}); + try { + await github.issues.createComment({owner, repo, number, body}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during creating comment on issue.'); + } logger.debug(`Github comment is added on issue with message: "${body}"`); } @@ -207,9 +227,13 @@ async function markIssueAsPaid(copilot, repo, number, challengeId) { const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); const labels = [config.PAID_ISSUE_LABEL, config.FIX_ACCEPTED_ISSUE_LABEL]; - await github.issues.edit({owner, repo, number, labels}); - const body = `Payment task has been updated: ${config.TC_OR_DETAIL_LINK}${challengeId}`; - await github.issues.createComment({owner, repo, number, body}); + try { + await github.issues.edit({owner, repo, number, labels}); + const body = `Payment task has been updated: ${config.TC_OR_DETAIL_LINK}${challengeId}`; + await github.issues.createComment({owner, repo, number, body}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during updating issue as paid.'); + } logger.debug(`Github issue title is updated for as paid and fix accepted for ${number}`); } @@ -231,7 +255,11 @@ async function changeState(copilot, repo, number, state) { Joi.attempt({copilot, repo, number, state}, changeState.schema); const github = await _authenticate(copilot.accessToken); const owner = await _getUsernameById(github, copilot.userProviderId); - await github.issues.edit({owner, repo, number, state}); + try { + await github.issues.edit({owner, repo, number, state}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during updating status of issue.'); + } logger.debug(`Github issue state is updated to '${state}' for issue number ${number}`); } @@ -242,6 +270,32 @@ changeState.schema = { state: Joi.string().required() }; +/** + * updates the github issue with new labels + * @param {Object} copilot the copilot + * @param {string} repo the repository + * @param {Number} number the issue number + * @param {Number} labels the challenge id + */ +async function addLabels(copilot, repo, number, labels) { + Joi.attempt({copilot, repo, number, labels}, addLabels.schema); + const github = await _authenticate(copilot.accessToken); + const owner = await _getUsernameById(github, copilot.userProviderId); + try { + await github.issues.edit({owner, repo, number, labels}); + } catch (err) { + throw errors.convertGitHubError(err, 'Error occurred during adding label in issue.'); + } + logger.debug(`Github issue is updated with new labels for ${number}`); +} + +addLabels.schema = { + copilot: copilotUserSchema, + repo: Joi.string().required(), + number: Joi.number().required(), + labels: Joi.array().items(Joi.string()).required() +}; + module.exports = { updateIssue, assignUser, @@ -250,5 +304,8 @@ module.exports = { getUsernameById, getUserIdByLogin, markIssueAsPaid, - changeState + changeState, + addLabels }; + +logger.buildService(module.exports); diff --git a/services/GitlabService.js b/services/GitlabService.js index f927c9c..55b71f6 100644 --- a/services/GitlabService.js +++ b/services/GitlabService.js @@ -14,6 +14,7 @@ const Joi = require('joi'); const config = require('config'); const GitlabAPI = require('node-gitlab-api'); const logger = require('../utils/logger'); +const errors = require('../utils/errors'); const copilotUserSchema = Joi.object().keys({ accessToken: Joi.string().required(), @@ -28,11 +29,15 @@ const copilotUserSchema = Joi.object().keys({ * @private */ async function _authenticate(accessToken) { - const gitlab = GitlabAPI({ - url: config.GITLAB_API_BASE_URL, - oauthToken: accessToken - }); - return gitlab; + try { + const gitlab = GitlabAPI({ + url: config.GITLAB_API_BASE_URL, + oauthToken: accessToken + }); + return gitlab; + } catch (err) { + throw errors.convertGitLabError(err, 'Failed to during authenticate to Github using access token of copilot.'); + } } /** @@ -44,9 +49,13 @@ async function _authenticate(accessToken) { * @private */ async function _removeAssignees(gitlab, projectId, issueId, assignees) { - const issue = await gitlab.projects.issues.show(projectId, issueId); - const oldAssignees = _.difference(issue.assignee_ids, assignees); - await gitlab.projects.issues.edit(projectId, issueId, {assignee_ids: oldAssignees}); + try { + const issue = await gitlab.projects.issues.show(projectId, issueId); + const oldAssignees = _.difference(issue.assignee_ids, assignees); + await gitlab.projects.issues.edit(projectId, issueId, {assignee_ids: oldAssignees}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during remove assignees from issue.'); + } } /** @@ -59,7 +68,11 @@ async function _removeAssignees(gitlab, projectId, issueId, assignees) { async function createComment(copilot, projectId, issueId, body) { Joi.attempt({copilot, projectId, issueId, body}, createComment.schema); const gitlab = await _authenticate(copilot.accessToken); - await gitlab.projects.issues.notes.create(projectId, issueId, {body}); + try { + await gitlab.projects.issues.notes.create(projectId, issueId, {body}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during creating comment on issue.'); + } logger.debug(`Gitlab comment is added on issue with message: "${body}"`); } @@ -80,7 +93,11 @@ createComment.schema = { async function updateIssue(copilot, projectId, issueId, title) { Joi.attempt({copilot, projectId, issueId, title}, updateIssue.schema); const gitlab = await _authenticate(copilot.accessToken); - await gitlab.projects.issues.edit(projectId, issueId, {title}); + try { + await gitlab.projects.issues.edit(projectId, issueId, {title}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during updating issue.'); + } logger.debug(`Gitlab issue title is updated for issue number ${issueId}`); } @@ -101,12 +118,16 @@ updateIssue.schema = { async function assignUser(copilot, projectId, issueId, userId) { Joi.attempt({copilot, projectId, issueId, userId}, assignUser.schema); const gitlab = await _authenticate(copilot.accessToken); - const issue = await gitlab.projects.issues.show(projectId, issueId); - const oldAssignees = _.without(issue.assignee_ids, userId); - if (oldAssignees && oldAssignees.length > 0) { - await _removeAssignees(gitlab, projectId, issueId, oldAssignees); + try { + const issue = await gitlab.projects.issues.show(projectId, issueId); + const oldAssignees = _.without(issue.assignee_ids, userId); + if (oldAssignees && oldAssignees.length > 0) { + await _removeAssignees(gitlab, projectId, issueId, oldAssignees); + } + await gitlab.projects.issues.edit(projectId, issueId, {assignee_ids: [userId]}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during assigning issue user.'); } - await gitlab.projects.issues.edit(projectId, issueId, {assignee_ids: [userId]}); logger.debug(`Gitlab issue with number ${issueId} is assigned to ${issueId}`); } @@ -179,9 +200,13 @@ getUserIdByLogin.schema = { async function markIssueAsPaid(copilot, projectId, issueId, challengeId) { Joi.attempt({copilot, projectId, issueId, challengeId}, markIssueAsPaid.schema); const gitlab = await _authenticate(copilot.accessToken); - await gitlab.projects.issues.edit(projectId, issueId, {labels: `${config.PAID_ISSUE_LABEL},${config.FIX_ACCEPTED_ISSUE_LABEL}`}); - const body = `Payment task has been updated: ${config.TC_OR_DETAIL_LINK}${challengeId}`; - await gitlab.projects.issues.notes.create(projectId, issueId, {body}); + try { + await gitlab.projects.issues.edit(projectId, issueId, {labels: `${config.PAID_ISSUE_LABEL},${config.FIX_ACCEPTED_ISSUE_LABEL}`}); + const body = `Payment task has been updated: ${config.TC_OR_DETAIL_LINK}${challengeId}`; + await gitlab.projects.issues.notes.create(projectId, issueId, {body}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during updating issue as paid.'); + } logger.debug(`Gitlab issue is updated for as paid and fix accepted for ${issueId}`); } @@ -202,7 +227,11 @@ markIssueAsPaid.schema = { async function changeState(copilot, projectId, issueId, state) { Joi.attempt({copilot, projectId, issueId, state}, changeState.schema); const gitlab = await _authenticate(copilot.accessToken); - await gitlab.projects.issues.edit(projectId, issueId, {state_event: state}); + try { + await gitlab.projects.issues.edit(projectId, issueId, {state_event: state}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during updating status of issue.'); + } logger.debug(`Gitlab issue state is updated to '${state}' for issue number ${issueId}`); } @@ -213,6 +242,31 @@ changeState.schema = { state: Joi.string().required() }; +/** + * updates the gitlab issue with new labels + * @param {Object} copilot the copilot + * @param {string} projectId the project id + * @param {Number} issueId the issue issue id + * @param {Number} labels the labels + */ +async function addLabels(copilot, projectId, issueId, labels) { + Joi.attempt({copilot, projectId, issueId, labels}, addLabels.schema); + const gitlab = await _authenticate(copilot.accessToken); + try { + await gitlab.projects.issues.edit(projectId, issueId, {labels: _.join(labels, ',')}); + } catch (err) { + throw errors.convertGitLabError(err, 'Error occurred during adding label in issue.'); + } + logger.debug(`Gitlab issue is updated with new labels for ${issueId}`); +} + +addLabels.schema = { + copilot: copilotUserSchema, + projectId: Joi.number().positive().required(), + issueId: Joi.number().required(), + labels: Joi.array().items(Joi.string()).required() +}; + module.exports = { createComment, @@ -222,5 +276,8 @@ module.exports = { getUsernameById, getUserIdByLogin, markIssueAsPaid, - changeState + changeState, + addLabels }; + +logger.buildService(module.exports); diff --git a/services/IssueService.js b/services/IssueService.js index c36767e..1d20ea5 100755 --- a/services/IssueService.js +++ b/services/IssueService.js @@ -14,15 +14,16 @@ const _ = require('lodash'); const Joi = require('joi'); const MarkdownIt = require('markdown-it'); const config = require('config'); - const models = require('../models'); const logger = require('../utils/logger'); +const errors = require('../utils/errors'); const topcoderApiHelper = require('../utils/topcoder-api-helper'); const gitHubService = require('./GithubService'); const emailService = require('./EmailService'); const userService = require('./UserService'); const gitlabService = require('./GitlabService'); + const Issue = models.Issue; const md = new MarkdownIt(); @@ -52,6 +53,52 @@ function parsePrizes(issue) { issue.title = issue.title.replace(/^(\[.*\])/, ''); } +/** + * handles the event gracefully when there is error processing the event + * @param {Object} event the event + * @param {Object} issue the issue + * @param {Object} err the error + */ +async function handleEventGracefully(event, issue, err) { + if (err.errorAt === 'topcoder' || err.errorAt === 'processor') { + event.retryCount = _.toInteger(event.retryCount); + // reschedule event + if (event.retryCount <= config.RETRY_COUNT) { + logger.debug('Scheduling event for next retry'); + const newEvent = {...event}; + newEvent.retryCount += 1; + delete newEvent.copilot; + setTimeout(async () => { + const kafka = require('../utils/kafka'); // eslint-disable-line + await kafka.send(JSON.stringify(newEvent)); + logger.debug('The event is scheduled for retry'); + }, config.RETRY_INTERVAL); + } + let comment = `[${err.statusCode}]: ${err.message}`; + if (event.event === 'issue.closed' && event.paymentSuccessful === false) { + comment = `Payment failed: ${comment}`; + } + // notify error in git host + if (event.provider === 'github') { + await gitHubService.createComment(event.copilot, event.data.repository.name, issue.number, comment); + } else { + await gitlabService.createComment(event.copilot, event.data.repository.id, issue.number, comment); + } + if (event.event === 'issue.closed') { + // reopen + await reOpenIssue(event, issue); + // ensure label is ready for review + const readyForReviewLabels = [config.READY_FOR_REVIEW_ISSUE_LABEL]; + if (event.provider === 'github') { + await gitHubService.addLabels(event.copilot, event.data.repository.name, issue.number, readyForReviewLabels); + } else { + await gitlabService.addLabels(event.copilot, event.data.repository.id, issue.number, readyForReviewLabels); + } + } + } + throw err; +} + /** * check if challenge is exists for given issue in db/topcoder * @param {Object} issue the issue @@ -66,7 +113,7 @@ async function ensureChallengeExists(issue) { }); if (!dbIssue) { - throw new Error(`there is no challenge for the updated issue ${issue.number}`); + throw errors.internalDependencyError(`there is no challenge for the updated issue ${issue.number}`); } return dbIssue; } @@ -109,6 +156,19 @@ async function assignUserAsRegistrant(topcoderUserId, challengeId) { await topcoderApiHelper.addResourceToChallenge(challengeId, registrantBody); } +/** + * re opens the issue + * @param {Object} event the event + * @param {Object} issue the issue + */ +async function reOpenIssue(event, issue) { + if (event.provider === 'github') { + await gitHubService.changeState(event.copilot, event.data.repository.name, issue.number, 'open'); + } else { + await gitlabService.changeState(event.copilot, event.data.repository.id, issue.number, 'reopen'); + } +} + /** * removes the current assignee if user is not found in topcoder X mapping. * user first need to sign up in Topcoder X @@ -131,19 +191,17 @@ async function rollbackAssignee(event, assigneeUserId, issue, reOpen = false) { await gitHubService.createComment(event.copilot, event.data.repository.name, issue.number, comment); // un-assign the user from the ticket await gitHubService.removeAssign(event.copilot, event.data.repository.name, issue.number, assigneeUsername); - if (reOpen) { - await gitHubService.changeState(event.copilot, event.data.repository.name, issue.number, 'open'); - } } else { await gitlabService.createComment(event.copilot, event.data.repository.id, issue.number, comment); // un-assign the user from the ticket await gitlabService.removeAssign(event.copilot, event.data.repository.id, issue.number, assigneeUserId); - if (reOpen) { - await gitlabService.changeState(event.copilot, event.data.repository.id, issue.number, 'reopen'); - } + } + if (reOpen) { + await reOpenIssue(event, issue); } } + /** * Parse the comments from issue comment. * @param {Object} comment the comment @@ -193,14 +251,19 @@ async function handleIssueAssignment(event, issue) { logger.debug(`Looking up TC handle of git user: ${assigneeUserId}`); const userMapping = await userService.getTCUserName(event.provider, assigneeUserId); if (userMapping && userMapping.topcoderUsername) { - const dbIssue = await ensureChallengeExists(issue); - - logger.debug(`Getting the topcoder member ID for member name: ${userMapping.topcoderUsername}`); - const topcoderUserId = await topcoderApiHelper.getTopcoderMemberId(userMapping.topcoderUsername); - // Update the challenge - logger.debug(`Assigning user to challenge: ${userMapping.topcoderUsername}`); - assignUserAsRegistrant(topcoderUserId, dbIssue.challengeId); - + let dbIssue; + try { + dbIssue = await ensureChallengeExists(issue); + + logger.debug(`Getting the topcoder member ID for member name: ${userMapping.topcoderUsername}`); + const topcoderUserId = await topcoderApiHelper.getTopcoderMemberId(userMapping.topcoderUsername); + // Update the challenge + logger.debug(`Assigning user to challenge: ${userMapping.topcoderUsername}`); + assignUserAsRegistrant(topcoderUserId, dbIssue.challengeId); + } catch (err) { + handleEventGracefully(event, issue, err); + return; + } const contestUrl = getUrlForChallengeId(dbIssue.challengeId); const comment = `Contest ${contestUrl} has been updated - it has been assigned to ${userMapping.topcoderUsername}.`; if (event.provider === 'github') { @@ -256,21 +319,26 @@ async function handleIssueComment(event, issue) { * @private */ async function handleIssueUpdate(event, issue) { - const dbIssue = await ensureChallengeExists(issue); + let dbIssue; + try { + dbIssue = await ensureChallengeExists(issue); + + if (_.isMatch(dbIssue, issue)) { + // Title, body, prizes doesn't change, just ignore + logger.debug(`nothing changed for issue ${issue.number}`); + return; + } - if (_.isMatch(dbIssue, issue)) { - // Title, body, prizes doesn't change, just ignore - logger.debug(`nothing changed for issue ${issue.number}`); + // Update the challenge + await topcoderApiHelper.updateChallenge(dbIssue.challengeId, { + name: issue.title, + detailedRequirements: issue.body, + prizes: issue.prizes + }); + } catch (e) { + await handleEventGracefully(event, issue, e); return; } - - // Update the challenge - await topcoderApiHelper.updateChallenge(dbIssue.challengeId, { - name: issue.title, - detailedRequirements: issue.body, - prizes: issue.prizes - }); - // Save dbIssue.set({ title: issue.title, @@ -298,75 +366,89 @@ async function handleIssueUpdate(event, issue) { * @private */ async function handleIssueClose(event, issue) { - const dbIssue = await ensureChallengeExists(issue); - // if issue is closed without assignee then do nothing - if (!event.data.assignee.id) { - logger.debug(`This issue ${issue.number} doesn't have assignee so ignoring this event.`); + let dbIssue; + try { + dbIssue = await ensureChallengeExists(issue); + if (!event.paymentSuccessful) { + // if issue is closed without assignee then do nothing + if (!event.data.assignee.id) { + logger.debug(`This issue ${issue.number} doesn't have assignee so ignoring this event.`); + return; + } + // if issue has paid label don't process further + if (_.includes(event.data.issue.labels, config.PAID_ISSUE_LABEL)) { + logger.debug(`This issue ${issue.number} is already paid with challenge ${dbIssue.challengeId}`); + return; + } + + logger.debug(`Looking up TC handle of git user: ${event.data.assignee.id}`); + const assigneeMember = await userService.getTCUserName(event.provider, event.data.assignee.id); + + // no mapping is found for current assignee remove assign, re-open issue and make comment + // to assignee to login with Topcoder X + if (!(assigneeMember && assigneeMember.topcoderUsername)) { + await rollbackAssignee(event, event.data.assignee.id, issue, true); + } + + // get project detail from db + const project = await getProjectDetail(issue, event); + + logger.debug(`Getting the billing account ID for project ID: ${project.tcDirectId}`); + const accountId = await topcoderApiHelper.getProjectBillingAccountId(project.tcDirectId); + + logger.debug(`assigning the billing account id ${accountId} to challenge`); + + // adding assignees as well if it is missed/failed during update + // prize needs to be again set after adding billing account otherwise it won't let activate + const updateBody = { + billingAccountId: accountId, + prizes: issue.prizes + }; + await topcoderApiHelper.updateChallenge(dbIssue.challengeId, updateBody); + + logger.debug(`Getting the topcoder member ID for member name: ${assigneeMember.topcoderUsername}`); + const winnerId = await topcoderApiHelper.getTopcoderMemberId(assigneeMember.topcoderUsername); + + logger.debug(`Getting the topcoder member ID for copilot name : ${event.copilot.topcoderUsername}`); + // get copilot tc user id + const copilotTopcoderUserId = await topcoderApiHelper.getTopcoderMemberId(event.copilot.topcoderUsername); + + // role id 14 for copilot + const copilotResourceBody = { + roleId: 14, + resourceUserId: copilotTopcoderUserId, + phaseId: 0, + addNotification: true, + addForumWatch: true + }; + await topcoderApiHelper.addResourceToChallenge(dbIssue.challengeId, copilotResourceBody); + + // adding reg + await assignUserAsRegistrant(winnerId, dbIssue.challengeId); + + // activate challenge + await topcoderApiHelper.activateChallenge(dbIssue.challengeId); + + logger.debug(`close challenge with winner ${assigneeMember.topcoderUsername}(${winnerId})`); + await topcoderApiHelper.closeChallenge(dbIssue.challengeId, winnerId); + event.paymentSuccessful = true; + } + } catch (e) { + event.paymentSuccessful = event.paymentSuccessful === true; // if once paid shouldn't be false + await handleEventGracefully(event, issue, e, event.paymentSuccessful); return; } - // if issue has paid label don't process further - if (_.includes(event.data.issue.labels, config.PAID_ISSUE_LABEL)) { - logger.debug(`This issue ${issue.number} is already paid with challenge ${dbIssue.challengeId}`); + try { + logger.debug('update issue as paid'); + if (event.provider === 'github') { + await gitHubService.markIssueAsPaid(event.copilot, event.data.repository.name, issue.number, dbIssue.challengeId); + } else { + await gitlabService.markIssueAsPaid(event.copilot, event.data.repository.id, issue.number, dbIssue.challengeId); + } + } catch (e) { + await handleEventGracefully(event, issue, e, event.paymentSuccessful); return; } - - logger.debug(`Looking up TC handle of git user: ${event.data.assignee.id}`); - const assigneeMember = await userService.getTCUserName(event.provider, event.data.assignee.id); - - // no mapping is found for current assignee remove assign, re-open issue and make comment - // to assignee to login with Topcoder X - if (!(assigneeMember && assigneeMember.topcoderUsername)) { - await rollbackAssignee(event, event.data.assignee.id, issue, true); - } - - // get project detail from db - const project = await getProjectDetail(issue, event); - - logger.debug(`Getting the billing account ID for project ID: ${project.tcDirectId}`); - const accountId = await topcoderApiHelper.getProjectBillingAccountId(project.tcDirectId); - - logger.debug(`assigning the billing account id ${accountId} to challenge`); - - // adding assignees as well if it is missed/failed during update - // prize needs to be again set after adding billing account otherwise it won't let activate - const updateBody = { - billingAccountId: accountId, - prizes: issue.prizes - }; - await topcoderApiHelper.updateChallenge(dbIssue.challengeId, updateBody); - - logger.debug(`Getting the topcoder member ID for member name: ${assigneeMember.topcoderUsername}`); - const winnerId = await topcoderApiHelper.getTopcoderMemberId(assigneeMember.topcoderUsername); - - logger.debug(`Getting the topcoder member ID for copilot name : ${event.copilot.topcoderUsername}`); - // get copilot tc user id - const copilotTopcoderUserId = await topcoderApiHelper.getTopcoderMemberId(event.copilot.topcoderUsername); - - // role id 14 for copilot - const copilotResourceBody = { - roleId: 14, - resourceUserId: copilotTopcoderUserId, - phaseId: 0, - addNotification: true, - addForumWatch: true - }; - await topcoderApiHelper.addResourceToChallenge(dbIssue.challengeId, copilotResourceBody); - - // adding reg - await assignUserAsRegistrant(winnerId, dbIssue.challengeId); - - // activate challenge - await topcoderApiHelper.activateChallenge(dbIssue.challengeId); - - logger.debug(`close challenge with winner ${assigneeMember.topcoderUsername}(${winnerId})`); - await topcoderApiHelper.closeChallenge(dbIssue.challengeId, winnerId); - - logger.debug('update issue as paid'); - if (event.provider === 'github') { - await gitHubService.markIssueAsPaid(event.copilot, event.data.repository.name, issue.number, dbIssue.challengeId); - } else { - await gitlabService.markIssueAsPaid(event.copilot, event.data.repository.id, issue.number, dbIssue.challengeId); - } } @@ -398,18 +480,22 @@ async function handleIssueCreate(event, issue) { }// if existing found don't create a project const projectId = project.tcDirectId; logger.debug(`existing project was found with id ${projectId} for repository ${event.data.repository.full_name}`); - - // Create a new challenge - issue.challengeId = await topcoderApiHelper.createChallenge({ - name: issue.title, - projectId, - detailedRequirements: issue.body, - prizes: issue.prizes, - task: true - }); - - // Save - await Issue.create(issue); + try { + // Create a new challenge + issue.challengeId = await topcoderApiHelper.createChallenge({ + name: issue.title, + projectId, + detailedRequirements: issue.body, + prizes: issue.prizes, + task: true + }); + + // Save + await Issue.create(issue); + } catch (e) { + await handleEventGracefully(event, issue, e); + return; + } const contestUrl = getUrlForChallengeId(issue.challengeId); const comment = `Contest ${contestUrl} has been created for this ticket.`; @@ -498,10 +584,14 @@ process.schema = Joi.object().keys({ assignee: Joi.object().keys({ id: Joi.number().required().allow(null) }) - }).required() + }).required(), + retryCount: Joi.number().integer().default(0).optional(), + paymentSuccessful: Joi.boolean().default(false).optional() }); module.exports = { process }; + +logger.buildService(module.exports); diff --git a/services/UserService.js b/services/UserService.js index dcc6a04..5957799 100755 --- a/services/UserService.js +++ b/services/UserService.js @@ -12,6 +12,7 @@ const Joi = require('joi'); const config = require('config'); const models = require('../models'); +const logger = require('../utils/logger'); /** * gets the tc handle for given git user id from a mapping captured by Topcoder x tool @@ -93,3 +94,5 @@ module.exports = { getTCUserName, getRepositoryCopilot }; + +logger.buildService(module.exports); diff --git a/utils/errors.js b/utils/errors.js new file mode 100644 index 0000000..a27193e --- /dev/null +++ b/utils/errors.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018 TopCoder, Inc. All rights reserved. + */ + +/** + * Define errors. + * + * @author veshu + * @version 1.0 + */ +'use strict'; + +const _ = require('lodash'); +const constants = require('../constants'); + +// the error class wrapper +class ProcessorError extends Error { + constructor(statusCode, message, errorAt) { + super(message); + this.name = this.constructor.name; + this.statusCode = statusCode; + this.errorAt = errorAt; + Error.captureStackTrace(this, this.constructor); + } +} + +const errors = {}; + +/** +* Convert github api error. +* @param {Error} err the github api error +* @param {String} message the error message +* @returns {Error} converted error +*/ +errors.convertGitHubError = function convertGitHubError(err, message) { + let resMsg = `${message}. ${err.message}.`; + const detail = _.get(err, 'response.body.message'); + if (detail) { + resMsg += ` Detail: ${detail}`; + } + const apiError = new ProcessorError( + _.get(err, 'response.status', constants.SERVICE_ERROR_STATUS), + resMsg, + 'github' + ); + return apiError; +}; + +/** + * Convert gitlab api error. + * @param {Error} err the gitlab api error + * @param {String} message the error message + * @returns {Error} converted error + */ +errors.convertGitLabError = function convertGitLabError(err, message) { + let resMsg = `${message}. ${err.message}.`; + const detail = _.get(err, 'response.body.message'); + if (detail) { + resMsg += ` Detail: ${detail}`; + } + const apiError = new ProcessorError( + err.status || _.get(err, 'response.status', constants.SERVICE_ERROR_STATUS), + resMsg, + 'gitlab' + ); + return apiError; +}; + +/** + * Convert topcoder api error. + * @param {Error} err the topcoder api error + * @param {String} message the error message + * @returns {Error} converted error + */ +errors.convertTopcoderApiError = function convertTopcoderApiError(err, message) { + let resMsg = `${message}`; + const detail = _.get(err, 'response.body.result.content'); + if (detail) { + resMsg += ` Detail: ${detail}`; + } + const apiError = new ProcessorError( + err.status || _.get(err, 'response.body.result.status', constants.SERVICE_ERROR_STATUS), + resMsg, + 'topcoder' + ); + return apiError; +}; + +/** + * Convert internal error which needs to be handle gracefully. + * @param {String} message the error message + * @returns {Error} converted error + */ +errors.internalDependencyError = function internalDependencyError(message) { + const resMsg = `${message}`; + const apiError = new ProcessorError( + constants.SERVICE_ERROR_STATUS, + resMsg, + 'processor' + ); + return apiError; +}; + +module.exports = errors; diff --git a/utils/kafka.js b/utils/kafka.js index b978a98..39558f2 100644 --- a/utils/kafka.js +++ b/utils/kafka.js @@ -11,6 +11,7 @@ */ 'use strict'; +const {promisify} = require('util'); const kafka = require('kafka-node'); const config = require('config'); const _ = require('lodash'); @@ -25,7 +26,10 @@ class Kafka { this.consumer = new kafka.Consumer(this.client, [{topic: config.TOPIC, partition: config.PARTITION}], {autoCommit: true}); this.consumer.setOffset(config.TOPIC, 0, 0); this.offset = new Offset(this.client); + this.producer = new kafka.Producer(this.client); logger.info(`Connecting on topic: ${config.TOPIC}`); + + this.sendAsync = promisify(this.producer.send).bind(this.producer); } run() { @@ -66,6 +70,26 @@ class Kafka { .catch(logger.error); } }); + this.consumer.on('ready', () => { + logger.info('kafka consumer is ready.'); + }); + this.producer.on('ready', () => { + logger.info('kafka producer is ready.'); + + this.producer.createTopics([config.TOPIC], true, (err) => { + if (err) { + logger.error(`error in creating topic: ${config.TOPIC}, error: ${err.stack}`); + } else { + logger.info(`kafka topic: ${config.TOPIC} is ready`); + } + }); + }); + this.producer.on('error', (err) => { + logger.error(`kafka is not connected. ${err.stack}`); + }); + } + send(message) { + return this.sendAsync([{topic: config.TOPIC, messages: message}]); } } diff --git a/utils/logger.js b/utils/logger.js index 2e5f6bc..b9145ce 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -9,8 +9,11 @@ * @version 1.0 */ 'use strict'; +const util = require('util'); +const _ = require('lodash'); const winston = require('winston'); const config = require('config'); +const getParams = require('get-parameter-names'); const logger = new winston.Logger({ transports: [ @@ -33,5 +36,80 @@ logger.logFullError = function logFullError(err, signature) { err.logged = true; }; +/** + * Remove invalid properties from the object and hide long arrays + * @param {Object} obj the object + * @returns {Object} the new object with removed properties + * @private + */ +function sanitizeObject(obj) { + try { + return JSON.parse(JSON.stringify(obj, (name, value) => { + // Array of field names that should not be logged + const removeFields = ['refreshToken', 'accessToken']; + if (_.includes(removeFields, name)) { + return ''; + } + if (_.isArray(value) && value.length > 30) { // eslint-disable-line + return `Array(${value.length}`; + } + return value; + })); + } catch (e) { + return obj; + } +} + +/** + * Convert array with arguments to object + * @param {Array} params the name of parameters + * @param {Array} arr the array with values + * @returns {Object} converted object + * @private + */ +function combineObject(params, arr) { + const ret = {}; + _.forEach(arr, (arg, i) => { + ret[params[i]] = arg; + }); + return ret; +} + +/** + * Decorate all functions of a service and log debug information if DEBUG is enabled + * @param {Object} service the service + */ +logger.decorateWithLogging = function decorateWithLogging(service) { + if (config.LOG_LEVEL !== 'debug') { + return; + } + _.forEach(service, (method, name) => { + const params = method.params || getParams(method); + service[name] = async function serviceMethodWithLogging() { + logger.debug(`ENTER ${name}`); + logger.debug('input arguments'); + const args = Array.prototype.slice.call(arguments); // eslint-disable-line + logger.debug(util.inspect(sanitizeObject(combineObject(params, args)))); + try { + const result = await method.apply(this, arguments); // eslint-disable-line + logger.debug(`EXIT ${name}`); + logger.debug('output arguments'); + logger.debug(util.inspect(sanitizeObject(result))); + return result; + } catch (e) { + logger.logFullError(e, name); + throw e; + } + }; + }); +}; + +/** + * Apply logger and validation decorators + * @param {Object} service the service to wrap + */ +logger.buildService = function buildService(service) { + logger.decorateWithLogging(service); +}; module.exports = logger; diff --git a/utils/topcoder-api-helper.js b/utils/topcoder-api-helper.js index 8389324..dca2c72 100644 --- a/utils/topcoder-api-helper.js +++ b/utils/topcoder-api-helper.js @@ -25,6 +25,7 @@ const topcoderDevApiProjects = require('topcoder-dev-api-projects'); const topcoderDevApiChallenges = require('topcoder-dev-api-challenges'); const logger = require('./logger'); +const errors = require('./errors'); if (config.TC_DEV_ENV) { @@ -102,17 +103,20 @@ async function createProject(projectName) { const projectBody = new topcoderApiProjects.ProjectRequestBody.constructFromObject({ projectName }); - const projectResponse = await new Promise((resolve, reject) => { - projectsApiInstance.directProjectsPost(projectBody, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } + try { + const projectResponse = await new Promise((resolve, reject) => { + projectsApiInstance.directProjectsPost(projectBody, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); }); - }); - - return _.get(projectResponse, 'result.content.projectId'); + return _.get(projectResponse, 'result.content.projectId'); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to create project.'); + } } /** @@ -136,17 +140,21 @@ async function createChallenge(challenge) { submissionEndsAt: end }, challenge) }); - const challengeResponse = await new Promise((resolve, reject) => { - challengesApiInstance.saveDraftContest(challengeBody, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } + try { + const challengeResponse = await new Promise((resolve, reject) => { + challengesApiInstance.saveDraftContest(challengeBody, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); }); - }); - return _.get(challengeResponse, 'result.content.id'); + return _.get(challengeResponse, 'result.content.id'); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to create challenge.'); + } } /** @@ -161,18 +169,21 @@ async function updateChallenge(id, challenge) { const challengeBody = new topcoderApiChallenges.UpdateChallengeBodyParam.constructFromObject({ param: challenge }); - - await new Promise((resolve, reject) => { - challengesApiInstance.challengesIdPut(id, challengeBody, (err, res) => { - if (err) { - logger.error(err); - logger.debug(JSON.stringify(err)); - reject(err); - } else { - resolve(res); - } + try { + await new Promise((resolve, reject) => { + challengesApiInstance.challengesIdPut(id, challengeBody, (err, res) => { + if (err) { + logger.error(err); + logger.debug(JSON.stringify(err)); + reject(err); + } else { + resolve(res); + } + }); }); - }); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to update challenge.'); + } } /** @@ -182,13 +193,17 @@ async function updateChallenge(id, challenge) { async function activateChallenge(id) { const apiKey = await getAccessToken(); logger.debug(`Activating challenge ${id}`); - await axios.post(`${projectsClient.basePath}/challenges/${id}/activate`, null, { - headers: { - authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } - }); - logger.debug(`Challenge ${id} is activated successfully.`); + try { + await axios.post(`${projectsClient.basePath}/challenges/${id}/activate`, null, { + headers: { + authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + logger.debug(`Challenge ${id} is activated successfully.`); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to activate challenge.'); + } } /** @@ -199,13 +214,17 @@ async function activateChallenge(id) { async function closeChallenge(id, winnerId) { const apiKey = await getAccessToken(); logger.debug(`Closing challenge ${id}`); - await axios.post(`${projectsClient.basePath}/challenges/${id}/close?winnerId=${winnerId}`, null, { - headers: { - authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } - }); - logger.debug(`Challenge ${id} is closed successfully.`); + try { + await axios.post(`${projectsClient.basePath}/challenges/${id}/close?winnerId=${winnerId}`, null, { + headers: { + authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + logger.debug(`Challenge ${id} is closed successfully.`); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to close challenge.'); + } } /** @@ -216,12 +235,16 @@ async function closeChallenge(id, winnerId) { async function getProjectBillingAccountId(id) { const apiKey = await getAccessToken(); logger.debug(`Getting project billing detail ${id}`); - const response = await axios.get(`${projectsClient.basePath}/direct/projects/${id}`, { - headers: { - authorization: `Bearer ${apiKey}` - } - }); - return _.get(response, 'data.result.content.billingAccountIds[0]'); + try { + const response = await axios.get(`${projectsClient.basePath}/direct/projects/${id}`, { + headers: { + authorization: `Bearer ${apiKey}` + } + }); + return _.get(response, 'data.result.content.billingAccountIds[0]'); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to get billing detail for the project.'); + } } /** @@ -231,8 +254,12 @@ async function getProjectBillingAccountId(id) { */ async function getTopcoderMemberId(handle) { bearer.apiKey = await getAccessToken(); - const response = await axios.get(`${projectsClient.basePath}/members/${handle}`); - return _.get(response, 'data.result.content.userId'); + try { + const response = await axios.get(`${projectsClient.basePath}/members/${handle}`); + return _.get(response, 'data.result.content.userId'); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to get topcoder member id.'); + } } /** @@ -243,22 +270,26 @@ async function getTopcoderMemberId(handle) { async function addResourceToChallenge(id, resource) { bearer.apiKey = await getAccessToken(); logger.debug(`adding resource to challenge ${id}`); - await new Promise((resolve, reject) => { - challengesApiInstance.challengesIdResourcesPost(id, resource, (err, res) => { - if (err) { - if (_.get(JSON.parse(_.get(err, 'response.text')), 'result.content') - === `User ${resource.resourceUserId} with role ${resource.roleId} already exists`) { - resolve(); + try { + await new Promise((resolve, reject) => { + challengesApiInstance.challengesIdResourcesPost(id, resource, (err, res) => { + if (err) { + if (_.get(JSON.parse(_.get(err, 'response.text')), 'result.content') + === `User ${resource.resourceUserId} with role ${resource.roleId} already exists`) { + resolve(); + } else { + logger.error(JSON.stringify(err)); + reject(err); + } } else { - logger.error(JSON.stringify(err)); - reject(err); + logger.debug(`resource is added to challenge ${id} successfully.`); + resolve(res); } - } else { - logger.debug(`resource is added to challenge ${id} successfully.`); - resolve(res); - } + }); }); - }); + } catch (err) { + throw errors.convertTopcoderApiError(err, 'Failed to add resource to the challenge.'); + } } module.exports = {