@opentermsarchive/engine 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentermsarchive/engine",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Tracks and makes visible changes to the terms of online services",
5
5
  "homepage": "https://opentermsarchive.org",
6
6
  "bugs": {
@@ -28,6 +28,24 @@ export default class GitHub {
28
28
  const [ owner, repo ] = repository.split('/');
29
29
 
30
30
  this.commonParams = { owner, repo };
31
+
32
+ this.issuesCache = new Map();
33
+ this._issuesPromise = null;
34
+ }
35
+
36
+ get issues() {
37
+ if (!this._issuesPromise) {
38
+ logger.info('Loading issues from GitHub…');
39
+ this._issuesPromise = this.loadAllIssues();
40
+ }
41
+
42
+ return this._issuesPromise;
43
+ }
44
+
45
+ clearCache() {
46
+ this.issuesCache.clear();
47
+ this._issuesPromise = null;
48
+ logger.info('Issues cache cleared');
31
49
  }
32
50
 
33
51
  async initialize() {
@@ -53,6 +71,33 @@ export default class GitHub {
53
71
  }
54
72
  }
55
73
 
74
+ async loadAllIssues() {
75
+ try {
76
+ const issues = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
77
+ ...this.commonParams,
78
+ state: GitHub.ISSUE_STATE_ALL,
79
+ per_page: 100,
80
+ });
81
+
82
+ const onlyIssues = issues.filter(issue => !issue.pull_request); // Filter out pull requests since GitHub treats them as a special type of issue
83
+
84
+ onlyIssues.forEach(issue => {
85
+ const cachedIssue = this.issuesCache.get(issue.title);
86
+
87
+ if (!cachedIssue || new Date(issue.created_at) < new Date(cachedIssue.created_at)) { // Only work on the oldest issue if there are duplicates, in order to consolidate the longest history possible
88
+ this.issuesCache.set(issue.title, issue);
89
+ }
90
+ });
91
+
92
+ logger.info(`Cached ${onlyIssues.length} issues from the GitHub repository`);
93
+
94
+ return this.issuesCache;
95
+ } catch (error) {
96
+ logger.error(`Failed to load issues: ${error.message}`);
97
+ throw error;
98
+ }
99
+ }
100
+
56
101
  async getRepositoryLabels() {
57
102
  const { data: labels } = await this.octokit.request('GET /repos/{owner}/{repo}/labels', { ...this.commonParams });
58
103
 
@@ -68,6 +113,10 @@ export default class GitHub {
68
113
  });
69
114
  }
70
115
 
116
+ async getIssue(title) {
117
+ return (await this.issues).get(title);
118
+ }
119
+
71
120
  async createIssue({ title, description: body, labels }) {
72
121
  const { data: issue } = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
73
122
  ...this.commonParams,
@@ -76,43 +125,22 @@ export default class GitHub {
76
125
  labels,
77
126
  });
78
127
 
128
+ this.issuesCache.set(issue.title, issue);
129
+
79
130
  return issue;
80
131
  }
81
132
 
82
- async setIssueLabels({ issue, labels }) {
83
- await this.octokit.request('PUT /repos/{owner}/{repo}/issues/{issue_number}/labels', {
133
+ async updateIssue(issue, { state, labels }) {
134
+ const { data: updatedIssue } = await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
84
135
  ...this.commonParams,
85
136
  issue_number: issue.number,
137
+ state,
86
138
  labels,
87
139
  });
88
- }
89
-
90
- async openIssue(issue) {
91
- await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
92
- ...this.commonParams,
93
- issue_number: issue.number,
94
- state: GitHub.ISSUE_STATE_OPEN,
95
- });
96
- }
97
-
98
- async closeIssue(issue) {
99
- await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
100
- ...this.commonParams,
101
- issue_number: issue.number,
102
- state: GitHub.ISSUE_STATE_CLOSED,
103
- });
104
- }
105
-
106
- async getIssue({ title, ...searchParams }) {
107
- const issues = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
108
- ...this.commonParams,
109
- per_page: 100,
110
- ...searchParams,
111
- }, response => response.data);
112
140
 
113
- const [issue] = issues.filter(item => item.title === title); // since only one is expected, use the first one
141
+ this.issuesCache.set(updatedIssue.title, updatedIssue);
114
142
 
115
- return issue;
143
+ return updatedIssue;
116
144
  }
117
145
 
118
146
  async addCommentToIssue({ issue, comment: body }) {
@@ -127,24 +155,25 @@ export default class GitHub {
127
155
 
128
156
  async closeIssueWithCommentIfExists({ title, comment }) {
129
157
  try {
130
- const openedIssue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_OPEN });
158
+ const issue = await this.getIssue(title);
131
159
 
132
- if (!openedIssue) {
160
+ if (!issue || issue.state == GitHub.ISSUE_STATE_CLOSED) {
133
161
  return;
134
162
  }
135
163
 
136
- await this.addCommentToIssue({ issue: openedIssue, comment });
137
- await this.closeIssue(openedIssue);
164
+ await this.addCommentToIssue({ issue, comment });
165
+
166
+ const updatedIssue = await this.updateIssue(issue, { state: GitHub.ISSUE_STATE_CLOSED });
138
167
 
139
- return logger.info(`Closed issue #${openedIssue.number}: ${openedIssue.html_url}`);
168
+ logger.info(`Closed issue with comment #${updatedIssue.number}: ${updatedIssue.html_url}`);
140
169
  } catch (error) {
141
- logger.error(`Failed to update issue "${title}": ${error.message}`);
170
+ logger.error(`Failed to close issue with comment "${title}": ${error.stack}`);
142
171
  }
143
172
  }
144
173
 
145
174
  async createOrUpdateIssue({ title, description, label }) {
146
175
  try {
147
- const issue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_ALL });
176
+ const issue = await this.getIssue(title);
148
177
 
149
178
  if (!issue) {
150
179
  const createdIssue = await this.createIssue({ title, description, labels: [label] });
@@ -152,25 +181,21 @@ export default class GitHub {
152
181
  return logger.info(`Created issue #${createdIssue.number} "${title}": ${createdIssue.html_url}`);
153
182
  }
154
183
 
155
- if (issue.state == GitHub.ISSUE_STATE_CLOSED) {
156
- await this.openIssue(issue);
157
- logger.info(`Reopened issue #${issue.number}: ${issue.html_url}`);
158
- }
159
-
160
184
  const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
161
- const [managedLabel] = issue.labels.filter(label => managedLabelsNames.includes(label.name)); // it is assumed that only one specific reason for failure is possible at a time, making managed labels mutually exclusive
185
+ const labelsNotManagedToKeep = issue.labels.map(label => label.name).filter(label => !managedLabelsNames.includes(label));
186
+ const [managedLabel] = issue.labels.filter(label => managedLabelsNames.includes(label.name)); // It is assumed that only one specific reason for failure is possible at a time, making managed labels mutually exclusive
162
187
 
163
- if (managedLabel?.name == label) { // if the label is already assigned to the issue, the error is redundant with the one already reported and no further action is necessary
188
+ if (issue.state !== GitHub.ISSUE_STATE_CLOSED && managedLabel?.name === label) {
164
189
  return;
165
190
  }
166
191
 
167
- const labelsNotManagedToKeep = issue.labels.map(label => label.name).filter(label => !managedLabelsNames.includes(label));
192
+ const updatedIssue = await this.updateIssue(issue, { state: GitHub.ISSUE_STATE_OPEN, labels: [ label, ...labelsNotManagedToKeep ] });
168
193
 
169
- await this.setIssueLabels({ issue, labels: [ label, ...labelsNotManagedToKeep ] });
170
194
  await this.addCommentToIssue({ issue, comment: description });
171
- logger.info(`Updated issue #${issue.number}: ${issue.html_url}`);
195
+
196
+ logger.info(`Updated issue with comment #${updatedIssue.number}: ${updatedIssue.html_url}`);
172
197
  } catch (error) {
173
- logger.error(`Failed to update issue "${title}": ${error.message}`);
198
+ logger.error(`Failed to update issue "${title}": ${error.stack}`);
174
199
  }
175
200
  }
176
201
  }
@@ -12,10 +12,17 @@ describe('GitHub', function () {
12
12
 
13
13
  let MANAGED_LABELS;
14
14
  let github;
15
+ const EXISTING_OPEN_ISSUE = { number: 1, title: 'Opened issue', description: 'Issue description', state: GitHub.ISSUE_STATE_OPEN, labels: [{ name: 'location' }] };
16
+ const EXISTING_CLOSED_ISSUE = { number: 2, title: 'Closed issue', description: 'Issue description', state: GitHub.ISSUE_STATE_CLOSED, labels: [{ name: '403' }] };
15
17
 
16
- before(() => {
18
+ before(async () => {
17
19
  MANAGED_LABELS = require('./labels.json');
18
20
  github = new GitHub('owner/repo');
21
+ nock('https://api.github.com')
22
+ .get('/repos/owner/repo/issues')
23
+ .query(true)
24
+ .reply(200, [ EXISTING_OPEN_ISSUE, EXISTING_CLOSED_ISSUE ]);
25
+ await github.clearCache();
19
26
  });
20
27
 
21
28
  describe('#initialize', () => {
@@ -71,137 +78,88 @@ describe('GitHub', function () {
71
78
  });
72
79
 
73
80
  describe('#createLabel', () => {
74
- let scope;
75
81
  const LABEL = { name: 'new_label', color: 'ffffff' };
76
82
 
77
- before(async () => {
78
- scope = nock('https://api.github.com')
83
+ afterEach(nock.cleanAll);
84
+
85
+ it('creates the new label successfully', async () => {
86
+ const scope = nock('https://api.github.com')
79
87
  .post('/repos/owner/repo/labels', body => body.name === LABEL.name)
80
88
  .reply(200, LABEL);
81
89
 
82
90
  await github.createLabel(LABEL);
91
+ expect(scope.isDone()).to.be.true;
83
92
  });
84
93
 
85
- after(nock.cleanAll);
94
+ it('throws an error when creating a label fails', async () => {
95
+ nock('https://api.github.com')
96
+ .post('/repos/owner/repo/labels')
97
+ .reply(400, { message: 'Bad Request' });
86
98
 
87
- it('creates the new label', () => {
88
- expect(scope.isDone()).to.be.true;
99
+ await expect(github.createLabel(LABEL)).to.be.rejected;
89
100
  });
90
101
  });
91
102
 
92
103
  describe('#createIssue', () => {
93
104
  let scope;
94
105
  let result;
106
+
95
107
  const ISSUE = {
96
108
  title: 'New Issue',
97
109
  description: 'Description of the new issue',
98
- labels: ['bug'],
110
+ labels: [{ name: 'bug' }],
99
111
  };
100
112
  const CREATED_ISSUE = { number: 123, ...ISSUE };
101
113
 
102
114
  before(async () => {
103
115
  scope = nock('https://api.github.com')
104
- .post('/repos/owner/repo/issues', request => request.title === ISSUE.title && request.body === ISSUE.description && request.labels[0] === ISSUE.labels[0])
116
+ .post('/repos/owner/repo/issues', request =>
117
+ request.title === ISSUE.title && request.body === ISSUE.description && request.labels[0].name === ISSUE.labels[0].name)
105
118
  .reply(200, CREATED_ISSUE);
106
119
 
107
120
  result = await github.createIssue(ISSUE);
108
121
  });
109
122
 
110
- after(nock.cleanAll);
123
+ after(() => {
124
+ nock.cleanAll();
125
+ github.issuesCache.delete(ISSUE.title);
126
+ });
111
127
 
112
128
  it('creates the new issue', () => {
113
129
  expect(scope.isDone()).to.be.true;
114
130
  });
115
131
 
116
- it('returns the created issue', () => {
132
+ it('returns the newly created issue', () => {
117
133
  expect(result).to.deep.equal(CREATED_ISSUE);
118
134
  });
119
- });
120
-
121
- describe('#setIssueLabels', () => {
122
- let scope;
123
- const ISSUE_NUMBER = 123;
124
- const LABELS = [ 'bug', 'enhancement' ];
125
-
126
- before(async () => {
127
- scope = nock('https://api.github.com')
128
- .put(`/repos/owner/repo/issues/${ISSUE_NUMBER}/labels`, { labels: LABELS })
129
- .reply(200);
130
-
131
- await github.setIssueLabels({ issue: { number: ISSUE_NUMBER }, labels: LABELS });
132
- });
133
-
134
- after(nock.cleanAll);
135
-
136
- it('sets labels on the issue', () => {
137
- expect(scope.isDone()).to.be.true;
138
- });
139
- });
140
-
141
- describe('#openIssue', () => {
142
- let scope;
143
- const ISSUE = { number: 123 };
144
- const EXPECTED_REQUEST_BODY = { state: 'open' };
145
-
146
- before(async () => {
147
- scope = nock('https://api.github.com')
148
- .patch(`/repos/owner/repo/issues/${ISSUE.number}`, EXPECTED_REQUEST_BODY)
149
- .reply(200);
150
-
151
- await github.openIssue(ISSUE);
152
- });
153
-
154
- after(nock.cleanAll);
155
-
156
- it('opens the issue', () => {
157
- expect(scope.isDone()).to.be.true;
158
- });
159
- });
160
135
 
161
- describe('#closeIssue', () => {
162
- let scope;
163
- const ISSUE = { number: 123 };
164
- const EXPECTED_REQUEST_BODY = { state: 'closed' };
165
-
166
- before(async () => {
167
- scope = nock('https://api.github.com')
168
- .patch(`/repos/owner/repo/issues/${ISSUE.number}`, EXPECTED_REQUEST_BODY)
169
- .reply(200);
170
-
171
- await github.closeIssue(ISSUE);
172
- });
173
-
174
- after(nock.cleanAll);
136
+ it('throws error when creating issue fails', async () => {
137
+ nock('https://api.github.com')
138
+ .post('/repos/owner/repo/issues')
139
+ .reply(400, { message: 'Bad Request' });
175
140
 
176
- it('closes the issue', () => {
177
- expect(scope.isDone()).to.be.true;
141
+ await expect(github.createIssue(ISSUE)).to.be.rejected;
178
142
  });
179
143
  });
180
144
 
181
145
  describe('#getIssue', () => {
182
- let scope;
183
- let result;
184
-
185
- const ISSUE = { number: 123, title: 'Test Issue' };
186
- const ANOTHER_ISSUE = { number: 124, title: 'Test Issue 2' };
187
-
188
- before(async () => {
189
- scope = nock('https://api.github.com')
146
+ before(() => {
147
+ nock('https://api.github.com')
190
148
  .get('/repos/owner/repo/issues')
191
149
  .query(true)
192
- .reply(200, [ ISSUE, ANOTHER_ISSUE ]);
193
-
194
- result = await github.getIssue({ title: ISSUE.title });
150
+ .reply(200, [ EXISTING_OPEN_ISSUE, EXISTING_CLOSED_ISSUE ]);
195
151
  });
196
152
 
197
- after(nock.cleanAll);
198
-
199
- it('searches for the issue', () => {
200
- expect(scope.isDone()).to.be.true;
153
+ context('when the issue exists in the cache', () => {
154
+ it('returns the cached issue', async () => {
155
+ expect(await github.getIssue(EXISTING_OPEN_ISSUE.title)).to.deep.equal(EXISTING_OPEN_ISSUE);
156
+ });
201
157
  });
202
158
 
203
- it('returns the expected issue', () => {
204
- expect(result).to.deep.equal(ISSUE);
159
+ context('when the issue does not exist in the cache', () => {
160
+ it('returns undefined', async () => {
161
+ expect(await github.getIssue('Non-existent Issue')).to.be.undefined;
162
+ });
205
163
  });
206
164
  });
207
165
 
@@ -223,35 +181,33 @@ describe('GitHub', function () {
223
181
  it('adds the comment to the issue', () => {
224
182
  expect(scope.isDone()).to.be.true;
225
183
  });
184
+
185
+ it('throws an error when adding a comment fails', async () => {
186
+ nock('https://api.github.com')
187
+ .post(`/repos/owner/repo/issues/${ISSUE_NUMBER}/comments`)
188
+ .reply(400, { message: 'Bad Request' });
189
+
190
+ await expect(github.addCommentToIssue({ issue: { number: ISSUE_NUMBER }, comment: COMMENT })).to.be.rejected;
191
+ });
226
192
  });
227
193
 
228
194
  describe('#closeIssueWithCommentIfExists', () => {
229
195
  after(nock.cleanAll);
230
196
 
231
197
  context('when the issue exists and is open', () => {
232
- const ISSUE = {
233
- number: 123,
234
- title: 'Open Issue',
235
- state: GitHub.ISSUE_STATE_OPEN,
236
- };
237
198
  let addCommentScope;
238
199
  let closeIssueScope;
239
200
 
240
201
  before(async () => {
241
- nock('https://api.github.com')
242
- .get('/repos/owner/repo/issues')
243
- .query(true)
244
- .reply(200, [ISSUE]);
245
-
246
202
  addCommentScope = nock('https://api.github.com')
247
- .post(`/repos/owner/repo/issues/${ISSUE.number}/comments`)
203
+ .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`)
248
204
  .reply(200);
249
205
 
250
206
  closeIssueScope = nock('https://api.github.com')
251
- .patch(`/repos/owner/repo/issues/${ISSUE.number}`, { state: GitHub.ISSUE_STATE_CLOSED })
207
+ .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_CLOSED })
252
208
  .reply(200);
253
209
 
254
- await github.closeIssueWithCommentIfExists({ title: ISSUE.title, comment: 'Closing comment' });
210
+ await github.closeIssueWithCommentIfExists({ title: EXISTING_OPEN_ISSUE.title, comment: 'Closing comment' });
255
211
  });
256
212
 
257
213
  it('adds comment to the issue', () => {
@@ -264,29 +220,19 @@ describe('GitHub', function () {
264
220
  });
265
221
 
266
222
  context('when the issue exists and is closed', () => {
267
- const ISSUE = {
268
- number: 123,
269
- title: 'Closed Issue',
270
- state: GitHub.ISSUE_STATE_CLOSED,
271
- };
272
223
  let addCommentScope;
273
224
  let closeIssueScope;
274
225
 
275
226
  before(async () => {
276
- nock('https://api.github.com')
277
- .get('/repos/owner/repo/issues')
278
- .query(true)
279
- .reply(200, []);
280
-
281
227
  addCommentScope = nock('https://api.github.com')
282
- .post(`/repos/owner/repo/issues/${ISSUE.number}/comments`)
228
+ .post(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}/comments`)
283
229
  .reply(200);
284
230
 
285
231
  closeIssueScope = nock('https://api.github.com')
286
- .patch(`/repos/owner/repo/issues/${ISSUE.number}`, { state: GitHub.ISSUE_STATE_CLOSED })
232
+ .patch(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}`, { state: GitHub.ISSUE_STATE_CLOSED })
287
233
  .reply(200);
288
234
 
289
- await github.closeIssueWithCommentIfExists({ title: ISSUE.title, comment: 'Closing comment' });
235
+ await github.closeIssueWithCommentIfExists({ title: EXISTING_CLOSED_ISSUE.title, comment: 'Closing comment' });
290
236
  });
291
237
 
292
238
  it('does not add comment', () => {
@@ -303,11 +249,6 @@ describe('GitHub', function () {
303
249
  let closeIssueScope;
304
250
 
305
251
  before(async () => {
306
- nock('https://api.github.com')
307
- .get('/repos/owner/repo/issues')
308
- .query(true)
309
- .reply(200, []);
310
-
311
252
  addCommentScope = nock('https://api.github.com')
312
253
  .post(/\/repos\/owner\/repo\/issues\/\d+\/comments/)
313
254
  .reply(200);
@@ -330,12 +271,8 @@ describe('GitHub', function () {
330
271
  });
331
272
 
332
273
  describe('#createOrUpdateIssue', () => {
333
- before(async () => {
334
- nock('https://api.github.com')
335
- .get('/repos/owner/repo/labels')
336
- .reply(200, MANAGED_LABELS);
337
-
338
- await github.initialize();
274
+ before(() => {
275
+ github.MANAGED_LABELS = require('./labels.json');
339
276
  });
340
277
 
341
278
  context('when the issue does not exist', () => {
@@ -347,11 +284,6 @@ describe('GitHub', function () {
347
284
  };
348
285
 
349
286
  before(async () => {
350
- nock('https://api.github.com')
351
- .get('/repos/owner/repo/issues')
352
- .query(true)
353
- .reply(200, []); // Simulate that there is no issues on the repository
354
-
355
287
  createIssueScope = nock('https://api.github.com')
356
288
  .post('/repos/owner/repo/issues', {
357
289
  title: ISSUE_TO_CREATE.title,
@@ -363,58 +295,39 @@ describe('GitHub', function () {
363
295
  await github.createOrUpdateIssue(ISSUE_TO_CREATE);
364
296
  });
365
297
 
298
+ afterEach(() => {
299
+ github.issuesCache.delete(ISSUE_TO_CREATE.title);
300
+ });
301
+
366
302
  it('creates the issue', () => {
367
303
  expect(createIssueScope.isDone()).to.be.true;
368
304
  });
369
305
  });
370
306
 
371
307
  context('when the issue already exists', () => {
372
- const ISSUE = {
373
- title: 'Existing Issue',
374
- description: 'New comment',
375
- label: 'location',
376
- };
377
-
378
308
  context('when issue is closed', () => {
379
- let setIssueLabelsScope;
309
+ let updateIssueScope;
380
310
  let addCommentScope;
381
- let openIssueScope;
382
-
383
- const GITHUB_RESPONSE_FOR_EXISTING_ISSUE = {
384
- number: 123,
385
- title: ISSUE.title,
386
- description: ISSUE.description,
387
- labels: [{ name: 'selectors' }],
388
- state: GitHub.ISSUE_STATE_CLOSED,
389
- };
390
311
 
391
312
  before(async () => {
392
- nock('https://api.github.com')
393
- .get('/repos/owner/repo/issues')
394
- .query(true)
395
- .reply(200, [GITHUB_RESPONSE_FOR_EXISTING_ISSUE]);
396
-
397
- openIssueScope = nock('https://api.github.com')
398
- .patch(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN })
399
- .reply(200);
400
-
401
- setIssueLabelsScope = nock('https://api.github.com')
402
- .put(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}/labels`, { labels: ['location'] })
313
+ updateIssueScope = nock('https://api.github.com')
314
+ .patch(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: ['location'] })
403
315
  .reply(200);
404
316
 
405
317
  addCommentScope = nock('https://api.github.com')
406
- .post(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}/comments`, { body: ISSUE.description })
318
+ .post(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}/comments`, { body: EXISTING_CLOSED_ISSUE.description })
407
319
  .reply(200);
408
320
 
409
- await github.createOrUpdateIssue(ISSUE);
321
+ await github.createOrUpdateIssue({ title: EXISTING_CLOSED_ISSUE.title, description: EXISTING_CLOSED_ISSUE.description, label: 'location' });
410
322
  });
411
323
 
412
- it('reopens the issue', () => {
413
- expect(openIssueScope.isDone()).to.be.true;
324
+ after(() => {
325
+ github.issuesCache.delete(EXISTING_CLOSED_ISSUE.title);
326
+ github.issuesCache.set(EXISTING_CLOSED_ISSUE.title, EXISTING_CLOSED_ISSUE);
414
327
  });
415
328
 
416
- it("updates the issue's label", () => {
417
- expect(setIssueLabelsScope.isDone()).to.be.true;
329
+ it('reopens the issue and updates its labels', () => {
330
+ expect(updateIssueScope.isDone()).to.be.true;
418
331
  });
419
332
 
420
333
  it('adds comment to the issue', () => {
@@ -423,49 +336,63 @@ describe('GitHub', function () {
423
336
  });
424
337
 
425
338
  context('when issue is already opened', () => {
426
- let setIssueLabelsScope;
427
- let addCommentScope;
428
- let openIssueScope;
429
-
430
- const GITHUB_RESPONSE_FOR_EXISTING_ISSUE = {
431
- number: 123,
432
- title: ISSUE.title,
433
- description: ISSUE.description,
434
- labels: [{ name: 'selectors' }],
435
- state: GitHub.ISSUE_STATE_OPEN,
436
- };
437
-
438
- before(async () => {
439
- nock('https://api.github.com')
440
- .get('/repos/owner/repo/issues')
441
- .query(true)
442
- .reply(200, [GITHUB_RESPONSE_FOR_EXISTING_ISSUE]);
443
-
444
- openIssueScope = nock('https://api.github.com')
445
- .patch(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN })
446
- .reply(200);
447
-
448
- setIssueLabelsScope = nock('https://api.github.com')
449
- .put(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}/labels`, { labels: ['location'] })
450
- .reply(200);
451
-
452
- addCommentScope = nock('https://api.github.com')
453
- .post(`/repos/owner/repo/issues/${GITHUB_RESPONSE_FOR_EXISTING_ISSUE.number}/comments`, { body: ISSUE.description })
454
- .reply(200);
455
-
456
- await github.createOrUpdateIssue(ISSUE);
339
+ context('when the reason is new', () => {
340
+ let addCommentScope;
341
+ let updateIssueScope;
342
+
343
+ before(async () => {
344
+ updateIssueScope = nock('https://api.github.com')
345
+ .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: ['404'] })
346
+ .reply(200);
347
+
348
+ addCommentScope = nock('https://api.github.com')
349
+ .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`, { body: EXISTING_OPEN_ISSUE.description })
350
+ .reply(200);
351
+
352
+ await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, label: '404' });
353
+ });
354
+
355
+ after(() => {
356
+ github.issuesCache.delete(EXISTING_OPEN_ISSUE.title);
357
+ github.issuesCache.set(EXISTING_OPEN_ISSUE.title, EXISTING_OPEN_ISSUE);
358
+ });
359
+
360
+ it("updates the issue's labels", () => {
361
+ expect(updateIssueScope.isDone()).to.be.true;
362
+ });
363
+
364
+ it('adds a comment to the issue', () => {
365
+ expect(addCommentScope.isDone()).to.be.true;
366
+ });
457
367
  });
458
-
459
- it('does not change the issue state', () => {
460
- expect(openIssueScope.isDone()).to.be.false;
461
- });
462
-
463
- it("updates the issue's label", () => {
464
- expect(setIssueLabelsScope.isDone()).to.be.true;
465
- });
466
-
467
- it('adds comment to the issue', () => {
468
- expect(addCommentScope.isDone()).to.be.true;
368
+ context('when the reason did not change', () => {
369
+ let addCommentScope;
370
+ let updateIssueScope;
371
+
372
+ before(async () => {
373
+ updateIssueScope = nock('https://api.github.com')
374
+ .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: EXISTING_OPEN_ISSUE.labels })
375
+ .reply(200);
376
+
377
+ addCommentScope = nock('https://api.github.com')
378
+ .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`, { body: EXISTING_OPEN_ISSUE.description })
379
+ .reply(200);
380
+
381
+ await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, label: EXISTING_OPEN_ISSUE.labels[0].name });
382
+ });
383
+
384
+ after(() => {
385
+ github.issuesCache.delete(EXISTING_OPEN_ISSUE.title);
386
+ github.issuesCache.set(EXISTING_OPEN_ISSUE.title, EXISTING_OPEN_ISSUE);
387
+ });
388
+
389
+ it('does not attempt to updates the issue’s labels', () => {
390
+ expect(updateIssueScope.isDone()).to.be.false;
391
+ });
392
+
393
+ it('does not attempt to add any comment to the issue', () => {
394
+ expect(addCommentScope.isDone()).to.be.false;
395
+ });
469
396
  });
470
397
  });
471
398
  });
@@ -49,6 +49,10 @@ export default class Reporter {
49
49
  return this.github.initialize();
50
50
  }
51
51
 
52
+ onTrackingStarted() {
53
+ return this.github.clearCache();
54
+ }
55
+
52
56
  async onVersionRecorded(version) {
53
57
  await this.github.closeIssueWithCommentIfExists({
54
58
  title: Reporter.generateTitleID(version.serviceId, version.termsType),