This repository was archived by the owner on Mar 17, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
Add Cloud Functions #24
Open
steffnay
wants to merge
27
commits into
master
Choose a base branch
from
functions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
c8a20ab
added cloud functions
steffnay 8b65da1
mocked settings
steffnay 705f9c3
mocked settings
steffnay d43b5b6
mocked settings
steffnay 1ce050d
fixing util require
steffnay 5c33e2f
removed util require
steffnay 714f0cb
updated proxyquire
steffnay bb40659
updated proxyquire. again.
steffnay be743bd
removed unused from package.json, updated triage test
steffnay 2cfc3af
Update CI config.
jmdobry 0867195
Merge branch 'functions' of github.com:GoogleCloudPlatform/LabelCat i…
jmdobry c042958
Update README.md
steffnay beda025
DRYed up testing, fixed inconsistent variables, addressed PR comments
steffnay 193ae22
conflict resolved
steffnay 45234d9
changed name for default settings file
steffnay e949bcc
reverted beforeEach in functions_system_test
steffnay 2c22416
debug
steffnay 9104847
updated constants and other variable names for consistency
steffnay b5352e3
corrected variables, reverted some refctoring
steffnay 60f1526
linted
steffnay 19b9f21
Debug test
jmdobry 331a53a
Fix tests.
jmdobry 8f3b8dd
Fix lint.
jmdobry d2cf9fb
Merge branch 'master' into functions
jmdobry 05d2b90
cleaned up
steffnay 38f9f15
resolved conflict
steffnay 67cde48
added getMocks function to tests
steffnay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| node_modules/* | ||
| functions/node_modules/* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| const settings = require('./settings.json'); // eslint-disable-line node/no-missing-require | ||
steffnay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const crypto = require('crypto'); | ||
| const log = require('loglevel'); | ||
| log.setLevel('info'); | ||
| const automl = require('@google-cloud/automl'); | ||
| const PubSub = require(`@google-cloud/pubsub`); | ||
| const octokit = require('@octokit/rest')(); | ||
| const client = new automl.v1beta1.PredictionServiceClient(); | ||
|
|
||
| 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) { | ||
steffnay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
| } | ||
| } | ||
|
|
||
| async function triage(event) { | ||
| octokit.authenticate({ | ||
| type: 'oauth', | ||
| token: settings.SECRET_TOKEN, | ||
| }); | ||
|
|
||
| const pubSubMessage = event.data; | ||
| let issueData = Buffer.from(pubSubMessage.data, 'base64').toString(); | ||
| issueData = JSON.parse(issueData); | ||
| const owner = issueData.owner; | ||
| const repo = issueData.repo; | ||
| const number = issueData.number; | ||
|
|
||
| issueData.labeled = false; | ||
| let results = await predict(issueData.text); | ||
|
|
||
| if (results) { | ||
| const labels = ['bug']; | ||
| const response = await octokit.issues.addLabels({ | ||
| owner: owner, | ||
steffnay marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| repo: repo, | ||
| number: number, | ||
| labels: labels, | ||
| }); | ||
|
|
||
| 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, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "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", | ||
| "nconf": "^0.10.0" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| 'use strict'; | ||
|
|
||
| const fs = require('fs'); | ||
| 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 | ||
| const octokit = require('@octokit/rest')(); | ||
| const log = require('loglevel'); | ||
| const Papa = require('papaparse'); | ||
|
|
@@ -18,8 +18,7 @@ const automl = require(`@google-cloud/automl`); | |
| async function retrieveIssues(data, label, alternatives) { | ||
| octokit.authenticate({ | ||
| type: 'oauth', | ||
| key: settings.githubClientID, | ||
| secret: settings.githubClientSecret, | ||
| token: settings.SECRET_TOKEN, | ||
| }); | ||
|
|
||
| log.info('RETRIEVING ISSUES...'); | ||
|
|
@@ -106,7 +105,9 @@ function cleanLabels(issue, opts) { | |
| */ | ||
| function getIssueInfo(issue) { | ||
| try { | ||
| const text = issue.title + ' ' + issue.body; | ||
| const raw = issue.title + ' ' + issue.body; | ||
|
||
| // remove punctuation that will interfere with csv | ||
| const text = raw.replace(/[^\w\s]/gi, ''); | ||
steffnay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const labels = issue.labels.map(labelObject => labelObject.name); | ||
|
|
||
| return {text, labels}; | ||
|
|
@@ -134,20 +135,20 @@ function makeCSV(issues, file) { | |
|
|
||
| /** | ||
| * Create a Google AutoML Natural Language dataset | ||
| * @param {string} projectId | ||
| * @param {string} computeRegion | ||
| * @param {string} PROJECT_ID | ||
| * @param {string} COMPUTE_REGION | ||
| * @param {string} datasetName | ||
| * @param {string} multiLabel | ||
| */ | ||
| async function createDataset( | ||
| projectId, | ||
| computeRegion, | ||
| PROJECT_ID, | ||
| COMPUTE_REGION, | ||
| datasetName, | ||
| multiLabel | ||
| ) { | ||
| const automl = require(`@google-cloud/automl`); | ||
| const client = new automl.v1beta1.AutoMlClient(); | ||
| const projectLocation = client.locationPath(projectId, computeRegion); | ||
| const projectLocation = client.locationPath(PROJECT_ID, COMPUTE_REGION); | ||
|
|
||
| // Classification type is assigned based on multiClass value. | ||
| let classificationType = `MULTICLASS`; | ||
|
|
@@ -191,16 +192,20 @@ async function createDataset( | |
|
|
||
| /** | ||
| * Import data into Google AutoML NL dataset | ||
| * @param {string} projectId | ||
| * @param {string} computeRegion | ||
| * @param {string} PROJECT_ID | ||
| * @param {string} COMPUTE_REGION | ||
| * @param {string} datasetId | ||
| * @param {string} path | ||
| */ | ||
| async function importData(projectId, computeRegion, datasetId, path) { | ||
| async function importData(PROJECT_ID, COMPUTE_REGION, datasetId, path) { | ||
| const client = new automl.v1beta1.AutoMlClient(); | ||
|
|
||
| // Get the full path of the dataset. | ||
| const datasetFullId = client.datasetPath(projectId, computeRegion, datasetId); | ||
| const datasetFullId = client.datasetPath( | ||
| PROJECT_ID, | ||
| COMPUTE_REGION, | ||
| datasetId | ||
| ); | ||
|
|
||
| // Get the Google Cloud Storage URIs. | ||
| const inputUris = path.split(`,`); | ||
|
|
@@ -222,9 +227,9 @@ async function importData(projectId, computeRegion, datasetId, path) { | |
| } | ||
| } | ||
|
|
||
| async function listDatasets(projectId, computeRegion) { | ||
| async function listDatasets(PROJECT_ID, COMPUTE_REGION) { | ||
| const client = new automl.v1beta1.AutoMlClient(); | ||
| const projectLocation = client.locationPath(projectId, computeRegion); | ||
| const projectLocation = client.locationPath(PROJECT_ID, COMPUTE_REGION); | ||
|
|
||
| try { | ||
| const responses = await client.listDatasets({parent: projectLocation}); | ||
|
|
@@ -247,11 +252,11 @@ async function listDatasets(projectId, computeRegion) { | |
| } | ||
| } | ||
|
|
||
| async function createModel(projectId, computeRegion, datasetId, modelName) { | ||
| async function createModel(PROJECT_ID, COMPUTE_REGION, datasetId, modelName) { | ||
steffnay marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const client = new automl.v1beta1.AutoMlClient(); | ||
|
|
||
| // A resource that represents Google Cloud Platform location. | ||
| const projectLocation = client.locationPath(projectId, computeRegion); | ||
| const projectLocation = client.locationPath(PROJECT_ID, COMPUTE_REGION); | ||
|
|
||
| // Set model name and model metadata for the dataset. | ||
| const myModel = { | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.