cerebro-cli
Advanced tools
Comparing version 0.2.0 to 0.2.1
{ | ||
"name": "cerebro-cli", | ||
"version": "0.2.0", | ||
"version": "0.2.1", | ||
"description": "", | ||
@@ -8,3 +8,3 @@ "main": "./src/index.js", | ||
"coverage": "nyc npm run test", | ||
"lint": "standard", | ||
"lint": "standard && markdownlint README.md", | ||
"start": "node ./src/index.js", | ||
@@ -26,2 +26,4 @@ "test": "mocha" | ||
"devDependencies": { | ||
"markdownlint": "^0.23.1", | ||
"markdownlint-cli": "^0.27.1", | ||
"mocha": "^8.4.0", | ||
@@ -28,0 +30,0 @@ "nock": "^13.1.0", |
# Cerebro (cerebro-cli) | ||
> A novel recruiting tool using GitHub events. | ||
Finding capable developers is challenging. This tool starts with a simple heuristic - the ability | ||
to get a pull request (PR) merged given a sufficient amount of feedback, and filters from | ||
there. | ||
Finding capable developers is challenging. This tool starts with a simple | ||
heuristic - the ability to get a pull request (PR) merged given a sufficient | ||
amount of feedback, and filters from there. | ||
The flow is currently as follows: | ||
1. Listen to the public GitHub events firehose for pull request | ||
@@ -13,4 +15,4 @@ _merge_ events on PRs that have a specified number of comments. | ||
3. For each PR: | ||
1. Check if the language is your target language | ||
2. Check if the author of the PR is looking for a job | ||
1. Check if the language is your target language | ||
2. Check if the author of the PR is looking for a job | ||
@@ -28,4 +30,6 @@ ## Table of Contents | ||
- [Node.js](https://nodejs.org). The [nvm](https://nvm.sh) tool works well for this. | ||
- Optional, but highly recommended: A [GH personal token] with default permissions | ||
- [Node.js](https://nodejs.org). The [nvm](https://nvm.sh) tool works well for | ||
this. | ||
- Optional, but highly recommended: A [GH personal token] with default | ||
permissions | ||
- Optional: [Docker] and [Docker Compose]. | ||
@@ -39,3 +43,4 @@ | ||
Cerebro can be run in a number of different ways, always configured by environment variables. | ||
Cerebro can be run in a number of different ways, always configured by environment | ||
variables. | ||
@@ -46,11 +51,17 @@ ### Configuration | ||
- `LANGUAGES`: **Required.** Comma separated list of the target languages you're looking for | ||
- `GH_TOKEN`: **Not required but highly recommended.** Your GitHub personal authentication token. | ||
- `COMMENT_THRESHOLD`: _optional, default 3_. Show PRs with review comments greater than or equal to this number | ||
- `SHOW_NON_HIREABLE`: _optional, default false_. Show applicants that are not explicitly marked as hireable. | ||
- `CHANGESET_THRESHOLD`: _optional, default 5432_. Only match PRs that have a total changeset (additions + subtractions) under this number. | ||
- `LANGUAGES`: **Required.** Comma separated list of the target languages | ||
you're looking for | ||
- `GH_TOKEN`: **Not required but highly recommended.** Your GitHub personal | ||
authentication token. | ||
- `COMMENT_THRESHOLD`: _optional, default 3_. Show PRs with review comments | ||
greater than or equal to this number | ||
- `SHOW_NON_HIREABLE`: _optional, default false_. Show applicants that are not | ||
explicitly marked as hireable. | ||
- `CHANGESET_THRESHOLD`: _optional, default 5432_. Only match PRs that have a | ||
total changeset (additions + subtractions) under this number. | ||
### Using `npx` | ||
You can skip the whole installation process altogether and just run Cerebro using `npx` | ||
You can skip the whole installation process altogether and just run Cerebro | ||
using `npx` | ||
@@ -71,3 +82,3 @@ ```bash | ||
-e LANGUAGES=c++,javascript \ | ||
aphelionz/cerebro-cli:v0.1.0 | ||
aphelionz/cerebro-cli:v0.2.0 | ||
``` | ||
@@ -80,3 +91,3 @@ | ||
cerebro: | ||
image: aphelionz/cerebro-cli:v0.1.0 | ||
image: aphelionz/cerebro-cli:v0.2.0 | ||
environment: | ||
@@ -113,3 +124,3 @@ GH_TOKEN: XXXXX | ||
``` | ||
```bash | ||
git clone https://github.com/aphelionz/cerebro | ||
@@ -132,9 +143,11 @@ cd cerebro | ||
2. English proficiency | ||
1. Really needs a manual overview until we find / create a good enough tool for this | ||
1. Really needs a manual overview until we find / create a good enough tool | ||
for this | ||
2. Ideally would be any proficiency in language | ||
3. "Looking for a job" false negatives, and false positives too | ||
1. `hireable` is either null (false) or true. However null is the default because GH jobs is | ||
opt-in. So we only make a note of this for now. | ||
1. `hireable` is either null (false) or true. However null is the default | ||
because GH jobs is opt-in. So we only make a note of this for now. | ||
4. IPFS + OrbitDB integration? Or at least _some_ database | ||
5. Readline and raw stdin integration to make a proper UI (or just make an API + website) | ||
5. Readline and raw stdin integration to make a proper UI (or just make an | ||
API + website) | ||
6. Environment variable validation | ||
@@ -141,0 +154,0 @@ 1. Is it possible to get the full list of supported GH languages? |
@@ -35,4 +35,9 @@ const metrics = require('./metrics') | ||
}) | ||
seeker.events.on('candidateFound', output.console) | ||
function outputCandidate (candidate) { | ||
output.console(candidate) | ||
metrics.custom.candidatesFound.inc(1) | ||
} | ||
seeker.events.on('candidateFound', outputCandidate) | ||
// Start Prometheus metrics server on the specified port | ||
@@ -48,5 +53,5 @@ metrics.start({ port: 9100 }) | ||
console.log('stopping seeker...') | ||
seeker.events.off('candidateFound', output.console) | ||
seeker.events.off('candidateFound', outputCandidate) | ||
seeker.stop() | ||
process.exit() | ||
}) |
const { Octokit } = require('@octokit/rest') | ||
const EventEmitter = require('events') | ||
const events = new EventEmitter() | ||
const events = new EventEmitter() | ||
let commentThreshold | ||
let changeSetThreshold | ||
let seekInterval | ||
@@ -9,36 +11,14 @@ let octokit | ||
// Authenticated GitHub requests can be made 5000 times per | ||
// hour, which means one request can be made every 714.3ms | ||
// However, the public events feed isn't that busy so we just | ||
// round up to 1000ms or 1 second. | ||
// | ||
// Non-authenticated users can only make 60 requests per hour :( | ||
// TODO: Look into getting this from Octokit or the GH headers? | ||
function start (auth, { | ||
commentThreshold = 3, | ||
changeSetThreshold = 5432, | ||
showNonHireable = false, | ||
targetLanguages | ||
} = {}) { | ||
const interval = auth ? 1000 : 60000 | ||
octokit = new Octokit({ auth }) | ||
seekInterval = setInterval((function seek () { | ||
octokit.activity.listPublicEvents({ per_page: 100 }) | ||
.then(res => { | ||
return firstFilter(res.data, { commentThreshold, changeSetThreshold }) | ||
}) | ||
.then((data) => secondFilter(data, { targetLanguages, showNonHireable })) | ||
.catch(console.error) | ||
function getNewEvents (ghEvents) { | ||
const uniqueEvents = ghEvents.filter(d => parseInt(d.id, 10) > cursor) | ||
events.emit('_debug.uniqueEvents', uniqueEvents.length) | ||
return seek | ||
}()), interval) | ||
// Update cursor to greatest known ID | ||
cursor = ghEvents.map(e => parseInt(e.id, 10)).sort()[ghEvents.length - 1] | ||
return Promise.resolve(uniqueEvents) | ||
} | ||
function stop () { clearInterval(seekInterval) } | ||
function firstFilter (ghEvents, { commentThreshold, changeSetThreshold }) { | ||
const uniqueEvents = ghEvents.filter(d => parseInt(d.id, 10) > cursor) | ||
events.emit('_debug.uniqueEvents', uniqueEvents.length) | ||
const suitablePRs = uniqueEvents | ||
function getSuitablePRs (newEvents, { commentThreshold, changeSetThreshold }) { | ||
const suitablePRs = newEvents | ||
// Get merged pull requests | ||
.filter(d => d.type === 'PullRequestEvent') | ||
@@ -53,5 +33,9 @@ .filter(p => p.payload.action === 'closed') | ||
.filter(pr => pr.user.login.indexOf('bot') === -1) | ||
events.emit('_debug.suitablePRs', suitablePRs.length) | ||
return Promise.resolve(suitablePRs) | ||
} | ||
const filteredEvents = suitablePRs.map(pr => ({ | ||
async function formatSuitablePRs (suitablePRs) { | ||
const formattedPRs = suitablePRs.map(pr => ({ | ||
prHtmlUrl: pr.html_url, | ||
@@ -61,8 +45,32 @@ languagesUrl: pr.url.replace(/pulls(.*)$/g, 'languages'), | ||
})) | ||
events.emit('_debug.filteredEvents', filteredEvents.length) | ||
events.emit('_debug.filteredEvents', formattedPRs.length) | ||
return Promise.resolve(formattedPRs) | ||
} | ||
cursor = ghEvents.map(e => parseInt(e.id, 10)).sort()[ghEvents.length - 1] | ||
return Promise.resolve(filteredEvents) | ||
function start (auth, { | ||
commentThreshold = 3, | ||
changeSetThreshold = 5432, | ||
showNonHireable = false, | ||
targetLanguages | ||
} = {}) { | ||
// 5000 authenticated requests/hour (rounded up) or 60 for non-auth :( | ||
// TODO: Look into getting this from Octokit somehow? GH headers don't provide. | ||
const interval = auth ? 1000 : 60000 | ||
if(!octokit) { octokit = new Octokit({ auth }) } | ||
seekInterval = setInterval((function seek () { | ||
const rawEvents = octokit.activity.listPublicEvents({ per_page: 100 }) | ||
.then(res => res.data) | ||
.then(getNewEvents) | ||
.then(newEvents => getSuitablePRs(newEvents, { commentThreshold, changeSetThreshold })) | ||
.then(formatSuitablePRs) | ||
.then((data) => secondFilter(data, { targetLanguages, showNonHireable })) | ||
.catch(console.error) | ||
return seek | ||
}()), interval) // IIFE executes automatically | ||
} | ||
function stop () { clearInterval(seekInterval) } | ||
async function secondFilter (results, { targetLanguages, showNonHireable }) { | ||
@@ -81,3 +89,2 @@ for (let i = 0; i < results.length; i++) { | ||
// TODO: Refactor to "output" function of some sort | ||
const candidate = (await octokit.users.getByUsername({ username })).data | ||
@@ -84,0 +91,0 @@ if (candidate.hireable || showNonHireable) { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
45615
179
155
1
6