Skip to content

Commit

Permalink
Changes to get staging environment working
Browse files Browse the repository at this point in the history
While setting up the staging environment, I made these changes. Some are strictly necessary, like the namespace prefixing on fields. Some helped me find those issues, like returning errors from event handlers and logging more details in createWorkItem.

### Medium changes

(connection.js and services/Gus/*) Keep GUS api sessions alive for 10 minutes. And add debug logging for SOQL requests

(connection.js) Implement namespace prefixes on objects and fields. This should have been implemented when the app started supporting Agile Accelerator, which always has a namespace; not sure why this wasn't done already.

(GithubEvents/index.js and ghEvents.js) Wait for event handlers to return before returning from the github webhook. Previously webhooks would return synchronously, before any GUS callouts would be processed. Now, GUS callouts will delay the webhook response and can affect the 2xx/5xx status code. This will help track errors and latency through logs.

### Small changes

Add missing peer dependency 'winston'

Add logs in createWorkItem that helped diagnose an issue with bug priorities.

(GithubEvents/index.js) Use captureRejectionSymbol to mitigate an unhandled promise rejection and avoid app crash. Related to #161.
  • Loading branch information
Mike Senn committed Feb 26, 2023
1 parent 235360b commit e390482
Show file tree
Hide file tree
Showing 21 changed files with 198 additions and 189 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ smee -u <your smee address here> --path /webhook --port 1337

- Get your Record Type IDs from the Object Manager and copy the base URL from your Agile Accelerator, it should resemble the one included in the example.

- When you later set up Heroku Connect, your database table and fields related to Agile Accelerator may have a prefix, which you will set as the `SALESFORCE_PREFIX`
- When you later set up Heroku Connect, your database table and fields related to Agile Accelerator may be in a specific Postgres schema, which you will set as the `SALESFORCE_PREFIX`. If you use the defaults in Heroku Connect, this will be `salesforce.`

7. Add a link to your GitHub app (ex: the GitHub app for Salesforce's instance is https://github.com/apps/git2gus)

Expand All @@ -105,8 +105,8 @@ BUG_RECORD_TYPE_ID=NOPQRSTUVWXYZ
INVESTIGATION_RECORD_TYPE_ID=123456789012
WORK_ITEM_BASE_URL=https://myproject.lightning.force.com/lightning/r/ADM_Work__c/
GITHUB_APP_URL= https://github.com/apps/yourapplication
SALESFORCE_PREFIX=agf__
SALESFORCE_PREFIX=salesforce.
NAMESPACE_PREFIX=agf
```

For use with SSO-enabled organizations, you would also have additional lines:
Expand Down
4 changes: 2 additions & 2 deletions api/actions/__test__/createWorkItem.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jest.mock('../../services/Gus', () => ({
createWorkItemInGus: jest.fn(),
resolveBuild: jest.fn(),
getBugRecordTypeId: jest.fn(),
getById: jest.fn()
getById: jest.fn(),
field: name => name + '__c'
}));
jest.mock('../../actions/formatToGus', () => ({
formatToGus: jest.fn()
Expand Down Expand Up @@ -442,7 +443,6 @@ describe('createGusItem action', () => {
});

it('should create a comment without the url when the git2gus.config.hideWorkItemUrl = true', async () => {
expect.assertions(1);
Github.getRecordTypeId.mockReturnValue('bug');
Github.isSalesforceLabel.mockReturnValue(true);
Builds.resolveBuild.mockReturnValue(Promise.resolve('qwerty1234'));
Expand Down
3 changes: 2 additions & 1 deletion api/actions/__test__/createWorkItemForPR.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jest.mock('../../services/Gus', () => ({
createWorkItemInGus: jest.fn(),
resolveBuild: jest.fn(),
getBugRecordTypeId: jest.fn(),
getById: jest.fn()
getById: jest.fn(),
field: name => name + '__c'
}));
jest.mock('../../actions/formatToGus', () => ({
formatToGus: jest.fn()
Expand Down
59 changes: 34 additions & 25 deletions api/actions/createWorkItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,79 +14,88 @@ const { formatToGus } = require("./formatToGus");
const GithubEvents = require('../modules/GithubEvents');
const Github = require('../services/Github');
const Gus = require('../services/Gus');
const logger = require('../services/Logs/logger');

module.exports = {
eventName: GithubEvents.events.ISSUE_LABELED,
fn: async function (req) {
console.log('createWorkItem Action called with req: ', req);
logger.info('createWorkItem Action called');
const {
issue: { labels, html_url, body, milestone }
} = req.body;
let {
issue: { title }
} = req.body;
// Only grab the label being added for comparison against Salesforce labels
const { label : labelAdded } = req.body;
const { label: labelAdded } = req.body;
const { config } = req.git2gus;
const { hideWorkItemUrl } = config;
let productTag = config.productTag;
if (config.productTagLabels) {
console.log('createWorkItem will work with custom productTagLabels for issue titled: ', title);
logger.info('createWorkItem will work with custom productTagLabels for issue titled: ', title);
Object.keys(config.productTagLabels).forEach(productTagLabel => {
if (labels.some(label => label.name === productTagLabel)) {
productTag = config.productTagLabels[productTagLabel];
}
});
}
if(config.issueTypeLabels) {
console.log('createWorkItem will work with custom issueTypeLabels for issue titled: ', title);
if (config.issueTypeLabels) {
logger.info('createWorkItem will work with custom issueTypeLabels for issue titled: ', title);
Object.keys(config.issueTypeLabels).forEach(issueTypeLabel => {
// If the label added is a Salesforce custom label, give it the correct base label
if (labelAdded.name === issueTypeLabel) {
labelAdded.name = config.issueTypeLabels[issueTypeLabel];
}
if (labels.some(label => label.name === issueTypeLabel)) {
labels.push({name: config.issueTypeLabels[issueTypeLabel]});
labels.push({ name: config.issueTypeLabels[issueTypeLabel] });
}
});
}

let normalizedTitle = getTitleWithOptionalPrefix(config, title);
console.log('createWorkItem will create GUS work item with title: ', normalizedTitle);
logger.info('createWorkItem will create GUS work item with title: ', normalizedTitle);
// Only check the label being added
if (Github.isSalesforceLabel(labelAdded.name) && productTag) {
console.log('Verified valid label and product tag for issue titled: ', title);
logger.info('Verified valid label and product tag for issue titled: ', title);
const priority = Github.getPriority(labels);
console.log(`Found priority: ${priority} for issue titled: ${title}`);
logger.info(`Found priority: ${priority} for issue titled: ${title}`);
const recordTypeId = Github.getRecordTypeId(labels);
console.log(`Found recordTypeId: ${recordTypeId} for issue titled: ${title}`);
logger.info(`Found recordTypeId: ${recordTypeId} for issue titled: ${title}`);
const bodyInGusFormat = await formatToGus(html_url, body);
console.log(`Found bodyInGusFormat: ${bodyInGusFormat} for issue titled: ${title}`);
logger.info(`Found bodyInGusFormat: ${bodyInGusFormat} for issue titled: ${title}`);

console.log(`Using GUS Api to create workitem for issue titled: ${title}`);
logger.info(`Using GUS Api to create workitem for issue titled: ${title}`);
// default build to "undefined", to invoke our updateIssue error below
const buildName = milestone ? milestone.title : config.defaultBuild;
const foundInBuild = await Gus.resolveBuild(buildName);
console.log(`Found foundInBuild: ${foundInBuild} for issue titled: ${title}`);
logger.info(`Found foundInBuild: ${foundInBuild} for issue titled: ${title}`);

const issue = await Gus.getByRelatedUrl(html_url);
if (issue) {
logger.info(`Found existing Work "${issue.Name}" for issue "${html_url}"`);
} else {
logger.info(`No existing Work for issue "${html_url}"`);
}
const bugRecordTypeId = Gus.getBugRecordTypeId();

const alreadyLowestPriority =
issue && issue.Priority__c !== '' && issue.Priority__c <= priority;
const recordIdTypeIsSame = issue && issue.RecordTypeId === recordTypeId;
issue && issue[Gus.field('Priority')] !== '' && issue[Gus.field('Priority')] <= priority;
const recordTypeIdIsSame = issue && issue.RecordTypeId === recordTypeId;
const isRecordTypeBug = recordTypeId === bugRecordTypeId;

// If issue is a bug we check if it already exists and already has lowest priority
// If issue type is investigation or story, we simply check it exists
if (isRecordTypeBug && alreadyLowestPriority && recordIdTypeIsSame) {
if (isRecordTypeBug && alreadyLowestPriority && recordTypeIdIsSame) {
logger.info(`Not opening new bug because existing bug has lower priority`);
return;
} else if ( !isRecordTypeBug && recordIdTypeIsSame) {
} else if (!isRecordTypeBug && recordTypeIdIsSame) {
logger.info(`Not opening new bug because existing Work is another record type`);
return;
}

if (foundInBuild) {
try{
console.log('Calling GUS API to create a new work item');
try {
logger.info('Calling GUS API to create a new work item');
const syncedItem = await Gus.createWorkItemInGus(normalizedTitle,
bodyInGusFormat,
productTag,
Expand All @@ -96,21 +105,21 @@ module.exports = {
html_url,
recordTypeId);
const syncedItemFromGus = await Gus.getById(syncedItem.id);
console.log('###hideWorkItemUrl:' + hideWorkItemUrl);
logger.info('###hideWorkItemUrl:' + hideWorkItemUrl);
const displayUrl = (hideWorkItemUrl === 'true') ? syncedItemFromGus.Name : `[${syncedItemFromGus.Name}](${process.env.WORK_ITEM_BASE_URL + syncedItem.id}/view)`;
const msg = `This issue has been linked to a new work item: ${displayUrl}`;
console.log(msg, ' for issue titled: ', title);
logger.info(msg, ' for issue titled: ', title);
return await updateIssue(req, msg);
} catch(e) {
console.log(`Error while creating work item ${e.message}`);
} catch (e) {
logger.error(`Error while creating work item ${e.message}`, e);
return await updateIssue(req, 'Error while creating work item!');
}
} else {
console.log(`No correct build for issue titled: ${title}`);
logger.error(`No correct build for issue titled: ${title}`);
return await updateIssue(req, 'Error while creating work item. No valid build found in GUS!');
}
}
console.log('Failed to create work item for issue titled: ', title);
logger.error('Failed to create work item for issue titled: ', title);
return null;
}
};
7 changes: 4 additions & 3 deletions api/actions/createWorkItemForPR.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Gus = require('../services/Gus');
module.exports = {
eventName: GithubEvents.events.PULL_REQUEST_LABELED,
fn: async function(req) {
console.log('createWorkItem Action called with req: ', req);
console.log('createWorkItem Action called');
const {
pull_request: { labels, html_url, body, milestone }
} = req.body;
Expand Down Expand Up @@ -86,6 +86,7 @@ module.exports = {
console.log(
`Using GUS Api to create workitem for issue titled: ${title}`
);
// default build to "undefined", to invoke our updateIssue error below
const buildName = milestone ? milestone.title : config.defaultBuild;
const foundInBuild = await Gus.resolveBuild(buildName);
console.log(
Expand All @@ -97,8 +98,8 @@ module.exports = {

const alreadyLowestPriority =
issue &&
issue.Priority__c !== '' &&
issue.Priority__c <= priority;
issue[Gus.field('Priority')] !== '' &&
issue[Gus.field('Priority')] <= priority;
const recordIdTypeIsSame =
issue && issue.RecordTypeId === recordTypeId;
const isRecordTypeBug = recordTypeId === bugRecordTypeId;
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/GithubController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

module.exports = {
async processEvent(req, res) {
sails.config.ghEvents.emitFromReq(req);
await sails.config.ghEvents.emitFromReq(req);
return res.ok({
status: 'OK'
});
Expand Down
22 changes: 18 additions & 4 deletions api/modules/GithubEvents/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

const EventEmitter = require('events');
const { EventEmitter, captureRejectionSymbol } = require('events');
const logger = require('../../services/Logs/logger');

const events = {
Expand Down Expand Up @@ -71,6 +71,10 @@ const eventsConfig = {
};

class GithubEvents extends EventEmitter {
constructor() {
super({ captureRejections: true });
}

static match(req, eventName) {
const event = req.headers['x-github-event'];
const { action } = req.body;
Expand All @@ -80,13 +84,23 @@ class GithubEvents extends EventEmitter {
);
}

emitFromReq(req) {
async emitFromReq(req) {
const handlerPromises = [];

Object.keys(eventsConfig).forEach(eventName => {
if (GithubEvents.match(req, eventName)) {
logger.info('Request matches eventName', { req, eventName });
this.emit(eventName, req);
logger.info('Request matches eventName', { eventName });
this.emit(eventName, req, handlerPromises);
}
});

// wait for all handlers to finish, and throw if any rejections
await Promise.all(handlerPromises);
}

// Avoid unhandled promise rejection by logging rejections from event handlers
[captureRejectionSymbol](err, event, ...unusedArgs) {
logger.error(`unhandled async error in event ${event}`, err);
}
}

Expand Down
20 changes: 5 additions & 15 deletions api/services/Gus/closeWorkItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,17 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

const jsforce = require('jsforce');
const { getConnection, Work, field } = require('./connection');

module.exports = async function closeWorkItem(relatedUrl, status) {
const conn = new jsforce.Connection();
await conn.login(
process.env.GUS_USERNAME,
process.env.GUS_PASSWORD,
async err => {
if (err) {
return console.error(err);
}
}
);
const conn = await getConnection();
return Promise.resolve(
conn
.sobject('ADM_Work__c')
.find({ related_url__c: relatedUrl })
.sobject(Work)
.find({ [field('related_url')]: relatedUrl })
.update(
{
status__c: status
[field('status')]: status
},
(err, ret) => {
if (err || !ret.success) {
Expand Down
43 changes: 43 additions & 0 deletions api/services/Gus/connection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const jsforce = require('jsforce');
const logger = require('../../services/Logs/logger');

let connection;
async function getConnection() {
if (connection) {
return connection;
}

const conn = new jsforce.Connection({ logLevel: 'DEBUG' });
try {
await conn.login(process.env.GUS_USERNAME, process.env.GUS_PASSWORD);
connection = conn;

// Keep connection open, but do reconnect every so often
setTimeout(() => {
connection = null;
logger.info(`Forgetting Gus session`);
}, 10 * 60 * 1000); // 10 minutes

return conn;
} catch (err) {
logger.error('Error logging into GUS', err);
throw new Error(`Error logging into GUS ${err.message}`);
}
}

const NAMESPACE_PREFIX = process.env.NAMESPACE_PREFIX
? `${process.env.NAMESPACE_PREFIX}__`
: '';

function field(name) {
return `${NAMESPACE_PREFIX}${name}__c`;
}

module.exports = {
getConnection,
Work: NAMESPACE_PREFIX + 'ADM_Work__c',
Build: NAMESPACE_PREFIX + 'ADM_Build__c',
Changelist: NAMESPACE_PREFIX + 'ADM_Changelist__c',
prefix: NAMESPACE_PREFIX,
field
};
42 changes: 18 additions & 24 deletions api/services/Gus/createChangelistInGus.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,28 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
const { getConnection, Changelist, field } = require('./connection');

const jsforce = require('jsforce');

module.exports = function createChangelistInGus(
module.exports = async function createChangelistInGus(
relativeUrl,
issueId,
mergedAt
) {
const conn = new jsforce.Connection();
conn.login(process.env.GUS_USERNAME, process.env.GUS_PASSWORD, err => {
if (err) {
return console.error(err);
}
conn.sobject('ADM_Change_List__c').create(
{
Perforce_Changelist__c: relativeUrl,
Work__c: issueId,
External_ID__c: relativeUrl,
Source__c: 'GitHub',
Check_In_Date__c: mergedAt,
Check_In_By__c: process.env.GUS_USER
},
(err, ret) => {
if (err || !ret.success) {
return console.error(err, ret);
}
return ret.id;
const conn = await getConnection();
return conn.sobject(Changelist).create(
{
[field('Perforce_Changelist')]: relativeUrl,
[field('Work')]: issueId,
[field('External_ID')]: relativeUrl,
[field('Source')]: 'GitHub',
[field('Check_In_Date')]: mergedAt,
[field('Check_In_By')]: process.env.GUS_USER
},
(err, ret) => {
if (err || !ret.success) {
return console.error(err, ret);
}
);
});
return ret.id;
}
);
};
Loading

0 comments on commit e390482

Please sign in to comment.