Skip to content
This repository was archived by the owner on Mar 17, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c8a20ab
added cloud functions
steffnay Oct 8, 2018
8b65da1
mocked settings
steffnay Oct 24, 2018
705f9c3
mocked settings
steffnay Oct 24, 2018
d43b5b6
mocked settings
steffnay Oct 24, 2018
1ce050d
fixing util require
steffnay Oct 24, 2018
5c33e2f
removed util require
steffnay Oct 24, 2018
714f0cb
updated proxyquire
steffnay Oct 24, 2018
bb40659
updated proxyquire. again.
steffnay Oct 24, 2018
be743bd
removed unused from package.json, updated triage test
steffnay Oct 24, 2018
2cfc3af
Update CI config.
jmdobry Oct 24, 2018
0867195
Merge branch 'functions' of github.com:GoogleCloudPlatform/LabelCat i…
jmdobry Oct 24, 2018
c042958
Update README.md
steffnay Oct 26, 2018
beda025
DRYed up testing, fixed inconsistent variables, addressed PR comments
steffnay Nov 3, 2018
193ae22
conflict resolved
steffnay Nov 3, 2018
45234d9
changed name for default settings file
steffnay Nov 3, 2018
e949bcc
reverted beforeEach in functions_system_test
steffnay Nov 3, 2018
2c22416
debug
steffnay Nov 3, 2018
9104847
updated constants and other variable names for consistency
steffnay Nov 6, 2018
b5352e3
corrected variables, reverted some refctoring
steffnay Nov 6, 2018
60f1526
linted
steffnay Nov 6, 2018
19b9f21
Debug test
jmdobry Nov 14, 2018
331a53a
Fix tests.
jmdobry Nov 14, 2018
8f3b8dd
Fix lint.
jmdobry Nov 14, 2018
d2cf9fb
Merge branch 'master' into functions
jmdobry Nov 17, 2018
05d2b90
cleaned up
steffnay Nov 26, 2018
38f9f15
resolved conflict
steffnay Nov 26, 2018
67cde48
added getMocks function to tests
steffnay Dec 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
command: |-
mkdir -p /home/node/.npm-global
./.circleci/npm-install-retry.js
cd functions
../.circleci/npm-install-retry.js
cd ..
environment:
NPM_CONFIG_PREFIX: /home/node/.npm-global
- run:
Expand All @@ -59,7 +62,7 @@ jobs:
done
fi
- run: *npm_install_and_link
- run: cp defaultsettings.json settings.json
- run: cp functions/settings.tmpl.json functions/settings.json
- run:
name: Run the tests.
command: npm test
Expand Down
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/*
functions/node_modules/*
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ animal, that's why you need LabelCat.
1. `cd LabelCat`
1. `npm install`
1. `npm link .`
1. `cp defaultsettings.json settings.json` (`settings.json` is where you
1. `cd functions`

`cp settings.tmpl.json settings.json` (`settings.json` is where you
customize the app)
1. Modify `settings.json` as necessary.

Expand Down Expand Up @@ -169,6 +171,14 @@ Examples:

labelcat createModel 123ABCD456789 firstModel

## Deploy Cloud Functions
1. `cd Functions`
1. `npm install`
1. `cp settings.tmpl.json settings.json`
1. Modify `settings.json` as necessary.
1. `gcloud alpha functions deploy handleNewIssue --trigger-http --runtime nodejs8`
1. `gcloud functions deploy triage --runtime nodejs8 --trigger-resource YOUR-PUB/SUB-TOPIC-NAME --trigger-event google.pubsub.topic.publish
`
## Contributing

See [CONTRIBUTING][3].
Expand Down
4 changes: 2 additions & 2 deletions bin/labelcat.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

const util = require('../src/util.js');
const settings = require('../settings.json'); // eslint-disable-line node/no-missing-require
const settings = require('../functions/settings.json'); // eslint-disable-line node/no-missing-require

require(`yargs`)
.demand(1)
Expand Down Expand Up @@ -76,7 +76,7 @@ require(`yargs`)
)
.command(
`listDatasets`,
`Train an AutoML NL model using existing dataset.`,
`Lists all AutoML NL datasets for current Google Cloud Platform project.`,
{},
() => {
const projectId = settings.projectId;
Expand Down
6 changes: 0 additions & 6 deletions defaultsettings.json

This file was deleted.

16 changes: 16 additions & 0 deletions functions/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
144 changes: 144 additions & 0 deletions functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const settings = require('./settings.json'); // eslint-disable-line node/no-missing-require
const crypto = require('crypto');
const automl = require('@google-cloud/automl');
const PubSub = require(`@google-cloud/pubsub`);
const octokit = require('@octokit/rest')();
const client = new automl.v1beta1.PredictionServiceClient();
const log = require('loglevel');
log.setLevel('info');

const PROJECT_ID = settings.PROJECT_ID;
const COMPUTE_REGION = settings.COMPUTE_REGION;
const MODEL_ID = settings.MODEL_ID;
const TOPIC_NAME = settings.TOPIC_NAME;
const SECRET_TOKEN = settings.SECRET_TOKEN;
const SCORE_THRESHOLD = 70;

/**
* Verifies request has come from Github and publishes
* message to specified Pub/Sub topic.
*
* @param {object} req
* @param {object} res
*/
async function handleNewIssue(req, res) {
try {
if (req.body.action !== 'opened') {
res.status(400).send('Wrong action.');
return;
}

await validateRequest(req);
const messageId = await publishMessage(req);
res.status(200).send(messageId);
} catch (err) {
log.error(err.stack);
res.status(403).send({error: err.message});
}
}

function validateRequest(req) {
return Promise.resolve().then(() => {
const digest = crypto
.createHmac('sha1', SECRET_TOKEN)
.update(JSON.stringify(req.body))
.digest('hex');
if (req.headers['x-hub-signature'] !== `sha1=${digest}`) {
const error = new Error('Unauthorized');
error.statusCode = 403;
throw error;
}
});
}

async function publishMessage(req) {
try {
const text = `${req.body.issue.title} ${req.body.issue.body}`;
const data = JSON.stringify({
owner: req.body.repository.owner.login,
repo: req.body.repository.name,
number: req.body.issue.number,
text: text,
});
const dataBuffer = Buffer.from(data);

const pubsubClient = new PubSub({
PROJECT_ID: PROJECT_ID,
});

const response = await pubsubClient
.topic(TOPIC_NAME)
.publisher()
.publish(dataBuffer);
return response;
} catch (err) {
log.error('ERROR:', err);
}
}

/**
* receives message from handleNewIssue cloud function,
* runs label prediction using assigned Google AutoML Natural Language model
* @param {object} req
* @param {object} res
*/
async function triage(req) {
octokit.authenticate({
type: 'oauth',
token: settings.SECRET_TOKEN,
});

const pubSubMessage = req.data;
let issueData = Buffer.from(pubSubMessage.data, 'base64').toString();
issueData = JSON.parse(issueData);

issueData.labeled = false;
let results = await predict(issueData.text);

if (results) {
const response = await octokit.issues.addLabels({
owner: issueData.owner,
repo: issueData.repo,
number: issueData.number,
labels: ['bug'],
});

if (response.status === 200) {
issueData.labeled = true;
}
}

return issueData;
}

async function predict(text) {
const modelFullId = client.modelPath(PROJECT_ID, COMPUTE_REGION, MODEL_ID);

const payload = {
textSnippet: {
content: text,
mimeType: `text/plain`,
},
};

try {
const response = await client.predict({
name: modelFullId,
payload: payload,
params: {},
});

if (response[0].payload[1].classification.score > SCORE_THRESHOLD) {
return true;
}

return false;
} catch (err) {
log.error(err);
}
}

module.exports = {
handleNewIssue,
triage,
};
12 changes: 12 additions & 0 deletions functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "labelcat-functions",
"engines": {
"node": ">= 8.x"
},
"dependencies": {
"@google-cloud/automl": "^0.1.2",
"@google-cloud/pubsub": "^0.20.1",
"@octokit/rest": "^15.15.1",
"loglevel": "^1.6.1"
}
}
7 changes: 7 additions & 0 deletions functions/settings.tmpl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"secretToken": "YOUR GITHUB CLIENT ID HERE",
"projectId": "YOUR GCP PROJECT ID HERE",
"computeRegion": "YOUR GCP PROJECT COMPUTE REGION HERE",
"modelId": "YOUR AUTOML NL MODEL ID HERE",
"topicName": "YOUR PUB/SUB TOPIC NAME HERE"
}
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,39 @@
"node": ">= 8.x"
},
"scripts": {
"cover": "nyc --reporter=lcov mocha test/*.js && nyc report",
"lint": "eslint src/ system-test/ test/ bin/",
"prettier": "prettier --write bin/*.js src/*.js test/*.js system-test/*.js",
"cover": "nyc --reporter=lcov mocha test/*.js system-test/*.js && nyc report",
"lint": "eslint src/ system-test/ test/ bin/ functions/",
"prettier": "prettier --write bin/*.js src/*.js test/*.js system-test/*.js functions/*.js",
"system-test": "mocha system-test/*.js --timeout 600000",
"test-no-cover": "mocha test/*.js",
"test": "npm run cover"
},
"dependencies": {
"@google-cloud/automl": "^0.1.2",
"@octokit/rest": "^15.12.1",
"@octokit/rest": "^15.15.1",
"csv-write-stream": "^2.0.0",
"json2csv": "^4.2.1",
"loglevel": "^1.6.1",
"papaparse": "^4.6.1",
"yargs": "^12.0.2"
},
"devDependencies": {
"body-parser": "^1.18.3",
"codecov": "^3.0.2",
"crypto": "^1.0.1",
"eslint": "^5.0.0",
"eslint-config-prettier": "^3.0.0",
"eslint-plugin-node": "^7.0.0",
"eslint-plugin-prettier": "^2.6.0",
"express": "^4.16.4",
"intelli-espower-loader": "^1.0.1",
"mocha": "^5.2.0",
"nyc": "^13.0.0",
"power-assert": "^1.6.0",
"prettier": "^1.13.5",
"proxyquire": "^2.1.0",
"sinon": "^6.3.4"
"sinon": "^6.3.4",
"supertest": "^3.3.0",
"uuid": "^3.3.2"
}
}
Loading