Skip to content

Commit 1e29547

Browse files
author
Mike Senn
authored
Changes to get staging environment working (#166)
* Changes to get staging environment working Fixes #165. 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. * prettier rules say single quotes * use jsforce auto-refresh Also log and throw all rejections from handlers
1 parent 235360b commit 1e29547

23 files changed

+225
-203
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ smee -u <your smee address here> --path /webhook --port 1337
8585

8686
- 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.
8787

88-
- 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`
88+
- 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.`
8989

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

@@ -105,8 +105,8 @@ BUG_RECORD_TYPE_ID=NOPQRSTUVWXYZ
105105
INVESTIGATION_RECORD_TYPE_ID=123456789012
106106
WORK_ITEM_BASE_URL=https://myproject.lightning.force.com/lightning/r/ADM_Work__c/
107107
GITHUB_APP_URL= https://github.com/apps/yourapplication
108-
SALESFORCE_PREFIX=agf__
109-
108+
SALESFORCE_PREFIX=salesforce.
109+
NAMESPACE_PREFIX=agf
110110
```
111111

112112
For use with SSO-enabled organizations, you would also have additional lines:

api/actions/__test__/createWorkItem.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ jest.mock('../../services/Gus', () => ({
3030
createWorkItemInGus: jest.fn(),
3131
resolveBuild: jest.fn(),
3232
getBugRecordTypeId: jest.fn(),
33-
getById: jest.fn()
33+
getById: jest.fn(),
34+
field: name => name + '__c'
3435
}));
3536
jest.mock('../../actions/formatToGus', () => ({
3637
formatToGus: jest.fn()
@@ -442,7 +443,6 @@ describe('createGusItem action', () => {
442443
});
443444

444445
it('should create a comment without the url when the git2gus.config.hideWorkItemUrl = true', async () => {
445-
expect.assertions(1);
446446
Github.getRecordTypeId.mockReturnValue('bug');
447447
Github.isSalesforceLabel.mockReturnValue(true);
448448
Builds.resolveBuild.mockReturnValue(Promise.resolve('qwerty1234'));

api/actions/__test__/createWorkItemForPR.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ jest.mock('../../services/Gus', () => ({
3030
createWorkItemInGus: jest.fn(),
3131
resolveBuild: jest.fn(),
3232
getBugRecordTypeId: jest.fn(),
33-
getById: jest.fn()
33+
getById: jest.fn(),
34+
field: name => name + '__c'
3435
}));
3536
jest.mock('../../actions/formatToGus', () => ({
3637
formatToGus: jest.fn()

api/actions/createWorkItem.js

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,79 +14,88 @@ const { formatToGus } = require("./formatToGus");
1414
const GithubEvents = require('../modules/GithubEvents');
1515
const Github = require('../services/Github');
1616
const Gus = require('../services/Gus');
17+
const logger = require('../services/Logs/logger');
1718

1819
module.exports = {
1920
eventName: GithubEvents.events.ISSUE_LABELED,
2021
fn: async function (req) {
21-
console.log('createWorkItem Action called with req: ', req);
22+
logger.info('createWorkItem Action called');
2223
const {
2324
issue: { labels, html_url, body, milestone }
2425
} = req.body;
2526
let {
2627
issue: { title }
2728
} = req.body;
2829
// Only grab the label being added for comparison against Salesforce labels
29-
const { label : labelAdded } = req.body;
30+
const { label: labelAdded } = req.body;
3031
const { config } = req.git2gus;
3132
const { hideWorkItemUrl } = config;
3233
let productTag = config.productTag;
3334
if (config.productTagLabels) {
34-
console.log('createWorkItem will work with custom productTagLabels for issue titled: ', title);
35+
logger.info('createWorkItem will work with custom productTagLabels for issue titled: ', title);
3536
Object.keys(config.productTagLabels).forEach(productTagLabel => {
3637
if (labels.some(label => label.name === productTagLabel)) {
3738
productTag = config.productTagLabels[productTagLabel];
3839
}
3940
});
4041
}
41-
if(config.issueTypeLabels) {
42-
console.log('createWorkItem will work with custom issueTypeLabels for issue titled: ', title);
42+
if (config.issueTypeLabels) {
43+
logger.info('createWorkItem will work with custom issueTypeLabels for issue titled: ', title);
4344
Object.keys(config.issueTypeLabels).forEach(issueTypeLabel => {
4445
// If the label added is a Salesforce custom label, give it the correct base label
4546
if (labelAdded.name === issueTypeLabel) {
4647
labelAdded.name = config.issueTypeLabels[issueTypeLabel];
4748
}
4849
if (labels.some(label => label.name === issueTypeLabel)) {
49-
labels.push({name: config.issueTypeLabels[issueTypeLabel]});
50+
labels.push({ name: config.issueTypeLabels[issueTypeLabel] });
5051
}
5152
});
5253
}
5354

5455
let normalizedTitle = getTitleWithOptionalPrefix(config, title);
55-
console.log('createWorkItem will create GUS work item with title: ', normalizedTitle);
56+
logger.info('createWorkItem will create GUS work item with title: ', normalizedTitle);
5657
// Only check the label being added
5758
if (Github.isSalesforceLabel(labelAdded.name) && productTag) {
58-
console.log('Verified valid label and product tag for issue titled: ', title);
59+
logger.info('Verified valid label and product tag for issue titled: ', title);
5960
const priority = Github.getPriority(labels);
60-
console.log(`Found priority: ${priority} for issue titled: ${title}`);
61+
logger.info(`Found priority: ${priority} for issue titled: ${title}`);
6162
const recordTypeId = Github.getRecordTypeId(labels);
62-
console.log(`Found recordTypeId: ${recordTypeId} for issue titled: ${title}`);
63+
logger.info(`Found recordTypeId: ${recordTypeId} for issue titled: ${title}`);
6364
const bodyInGusFormat = await formatToGus(html_url, body);
64-
console.log(`Found bodyInGusFormat: ${bodyInGusFormat} for issue titled: ${title}`);
65+
logger.info(`Found bodyInGusFormat: ${bodyInGusFormat} for issue titled: ${title}`);
6566

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

7173
const issue = await Gus.getByRelatedUrl(html_url);
74+
if (issue) {
75+
logger.info(`Found existing Work "${issue.Name}" for issue "${html_url}"`);
76+
} else {
77+
logger.info(`No existing Work for issue "${html_url}"`);
78+
}
7279
const bugRecordTypeId = Gus.getBugRecordTypeId();
7380

7481
const alreadyLowestPriority =
75-
issue && issue.Priority__c !== '' && issue.Priority__c <= priority;
76-
const recordIdTypeIsSame = issue && issue.RecordTypeId === recordTypeId;
82+
issue && issue[Gus.field('Priority')] !== '' && issue[Gus.field('Priority')] <= priority;
83+
const recordTypeIdIsSame = issue && issue.RecordTypeId === recordTypeId;
7784
const isRecordTypeBug = recordTypeId === bugRecordTypeId;
7885

7986
// If issue is a bug we check if it already exists and already has lowest priority
8087
// If issue type is investigation or story, we simply check it exists
81-
if (isRecordTypeBug && alreadyLowestPriority && recordIdTypeIsSame) {
88+
if (isRecordTypeBug && alreadyLowestPriority && recordTypeIdIsSame) {
89+
logger.info(`Not opening new bug because existing bug has lower priority`);
8290
return;
83-
} else if ( !isRecordTypeBug && recordIdTypeIsSame) {
91+
} else if (!isRecordTypeBug && recordTypeIdIsSame) {
92+
logger.info(`Not opening new bug because existing Work is another record type`);
8493
return;
8594
}
8695

8796
if (foundInBuild) {
88-
try{
89-
console.log('Calling GUS API to create a new work item');
97+
try {
98+
logger.info('Calling GUS API to create a new work item');
9099
const syncedItem = await Gus.createWorkItemInGus(normalizedTitle,
91100
bodyInGusFormat,
92101
productTag,
@@ -96,21 +105,21 @@ module.exports = {
96105
html_url,
97106
recordTypeId);
98107
const syncedItemFromGus = await Gus.getById(syncedItem.id);
99-
console.log('###hideWorkItemUrl:' + hideWorkItemUrl);
108+
logger.info('###hideWorkItemUrl:' + hideWorkItemUrl);
100109
const displayUrl = (hideWorkItemUrl === 'true') ? syncedItemFromGus.Name : `[${syncedItemFromGus.Name}](${process.env.WORK_ITEM_BASE_URL + syncedItem.id}/view)`;
101110
const msg = `This issue has been linked to a new work item: ${displayUrl}`;
102-
console.log(msg, ' for issue titled: ', title);
111+
logger.info(msg, ' for issue titled: ', title);
103112
return await updateIssue(req, msg);
104-
} catch(e) {
105-
console.log(`Error while creating work item ${e.message}`);
113+
} catch (e) {
114+
logger.error(`Error while creating work item ${e.message}`, e);
106115
return await updateIssue(req, 'Error while creating work item!');
107116
}
108117
} else {
109-
console.log(`No correct build for issue titled: ${title}`);
118+
logger.error(`No correct build for issue titled: ${title}`);
110119
return await updateIssue(req, 'Error while creating work item. No valid build found in GUS!');
111120
}
112121
}
113-
console.log('Failed to create work item for issue titled: ', title);
122+
logger.error('Failed to create work item for issue titled: ', title);
114123
return null;
115124
}
116125
};

api/actions/createWorkItemForPR.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const Gus = require('../services/Gus');
1818
module.exports = {
1919
eventName: GithubEvents.events.PULL_REQUEST_LABELED,
2020
fn: async function(req) {
21-
console.log('createWorkItem Action called with req: ', req);
21+
console.log('createWorkItem Action called');
2222
const {
2323
pull_request: { labels, html_url, body, milestone }
2424
} = req.body;
@@ -86,6 +86,7 @@ module.exports = {
8686
console.log(
8787
`Using GUS Api to create workitem for issue titled: ${title}`
8888
);
89+
// default build to "undefined", to invoke our updateIssue error below
8990
const buildName = milestone ? milestone.title : config.defaultBuild;
9091
const foundInBuild = await Gus.resolveBuild(buildName);
9192
console.log(
@@ -97,8 +98,8 @@ module.exports = {
9798

9899
const alreadyLowestPriority =
99100
issue &&
100-
issue.Priority__c !== '' &&
101-
issue.Priority__c <= priority;
101+
issue[Gus.field('Priority')] !== '' &&
102+
issue[Gus.field('Priority')] <= priority;
102103
const recordIdTypeIsSame =
103104
issue && issue.RecordTypeId === recordTypeId;
104105
const isRecordTypeBug = recordTypeId === bugRecordTypeId;

api/actions/integrateWorkItem.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ module.exports = {
2323
issue: { html_url }
2424
} = req.body;
2525
const { statusWhenClosed } = req.git2gus.config;
26-
closeWorkItem(html_url, getStatus(statusWhenClosed));
26+
await closeWorkItem(html_url, getStatus(statusWhenClosed));
2727
}
2828
};

api/controllers/GithubController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
module.exports = {
99
async processEvent(req, res) {
10-
sails.config.ghEvents.emitFromReq(req);
10+
await sails.config.ghEvents.emitFromReq(req);
1111
return res.ok({
1212
status: 'OK'
1313
});

api/modules/GithubEvents/index.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
const EventEmitter = require('events');
8+
const { EventEmitter } = require('events');
99
const logger = require('../../services/Logs/logger');
1010

1111
const events = {
@@ -80,13 +80,31 @@ class GithubEvents extends EventEmitter {
8080
);
8181
}
8282

83-
emitFromReq(req) {
83+
async emitFromReq(req) {
84+
const handlerPromises = [];
85+
8486
Object.keys(eventsConfig).forEach(eventName => {
8587
if (GithubEvents.match(req, eventName)) {
86-
logger.info('Request matches eventName', { req, eventName });
87-
this.emit(eventName, req);
88+
logger.info('Request matches eventName', { eventName });
89+
this.emit(eventName, req, handlerPromises);
8890
}
8991
});
92+
93+
const rejected = [];
94+
for (result of Promise.allSettled(handlerPromises)) {
95+
if (result.status === 'rejected') {
96+
rejected.push(result.reason);
97+
logger.error('Handler rejected', result.reason);
98+
} else {
99+
logger.info('Handler fulfilled', result.value);
100+
}
101+
}
102+
103+
if (rejected.length === 1) {
104+
throw rejected[0];
105+
} else if (rejected.length > 1) {
106+
throw new AggregateError(rejected);
107+
}
90108
}
91109
}
92110

api/services/Gus/closeWorkItem.js

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,31 @@
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
8-
const jsforce = require('jsforce');
7+
const { getConnection, Work, field } = require('./connection');
8+
const logger = require('../Logs/logger');
99

1010
module.exports = async function closeWorkItem(relatedUrl, status) {
11-
const conn = new jsforce.Connection();
12-
await conn.login(
13-
process.env.GUS_USERNAME,
14-
process.env.GUS_PASSWORD,
15-
async err => {
16-
if (err) {
17-
return console.error(err);
18-
}
19-
}
20-
);
21-
return Promise.resolve(
22-
conn
23-
.sobject('ADM_Work__c')
24-
.find({ related_url__c: relatedUrl })
25-
.update(
26-
{
27-
status__c: status
28-
},
29-
(err, ret) => {
30-
if (err || !ret.success) {
31-
return console.error(err, ret);
32-
}
33-
return ret;
34-
}
35-
)
36-
);
11+
const conn = await getConnection();
12+
let ret = await conn
13+
.sobject(Work)
14+
.find({ [field('related_url')]: relatedUrl })
15+
.update({
16+
[field('status')]: status
17+
});
18+
19+
// ret will already be an array if find() returned multiple work
20+
if (!Array.isArray(ret)) {
21+
ret = [ret];
22+
}
23+
24+
const errors = ret
25+
.filter(r => !r.success)
26+
.map(r => {
27+
logger.error(`Error updating work ${r.id}`, r.errors);
28+
return new Error(`Id ${r.id}: ${r.errors}`);
29+
});
30+
if (errors.length > 0) {
31+
throw new AggregateError(errors, 'Errors closing work');
32+
}
33+
return ret;
3734
};

api/services/Gus/connection.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const jsforce = require('jsforce');
2+
const logger = require('../../services/Logs/logger');
3+
4+
let connection;
5+
async function getConnection() {
6+
if (connection) {
7+
return connection;
8+
}
9+
10+
const conn = new jsforce.Connection({ logLevel: 'DEBUG' });
11+
try {
12+
// jsforce connection will auto-refresh if session expires
13+
await conn.login(process.env.GUS_USERNAME, process.env.GUS_PASSWORD);
14+
connection = conn;
15+
return conn;
16+
} catch (err) {
17+
logger.error('Error logging into GUS', err);
18+
throw new Error(`Error logging into GUS ${err.message}`);
19+
}
20+
}
21+
22+
const NAMESPACE_PREFIX = process.env.NAMESPACE_PREFIX
23+
? `${process.env.NAMESPACE_PREFIX}__`
24+
: '';
25+
26+
function field(name) {
27+
return `${NAMESPACE_PREFIX}${name}__c`;
28+
}
29+
30+
module.exports = {
31+
getConnection,
32+
Work: NAMESPACE_PREFIX + 'ADM_Work__c',
33+
Build: NAMESPACE_PREFIX + 'ADM_Build__c',
34+
Changelist: NAMESPACE_PREFIX + 'ADM_Changelist__c',
35+
prefix: NAMESPACE_PREFIX,
36+
field
37+
};

0 commit comments

Comments
 (0)