@opentermsarchive/engine 6.1.0 → 7.1.0

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/.eslintrc.yaml CHANGED
@@ -113,6 +113,7 @@ rules:
113
113
  - properties: false
114
114
  require-await: 1
115
115
  no-only-tests/no-only-tests: error
116
+ no-await-in-loop: 0 # allows intentional sequential processing of async operations
116
117
 
117
118
  overrides:
118
119
  - files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentermsarchive/engine",
3
- "version": "6.1.0",
3
+ "version": "7.1.0",
4
4
  "description": "Tracks and makes visible changes to the terms of online services",
5
5
  "homepage": "https://opentermsarchive.org",
6
6
  "bugs": {
@@ -140,7 +140,7 @@ async function rewriteSnapshots(repository, records, idsMapping, logger) {
140
140
  let i = 1;
141
141
 
142
142
  for (const record of records) {
143
- const { id: recordId } = await repository.save(record); // eslint-disable-line no-await-in-loop
143
+ const { id: recordId } = await repository.save(record);
144
144
 
145
145
  idsMapping[record.id] = recordId; // Saves the mapping between the old ID and the new one.
146
146
 
@@ -166,7 +166,7 @@ async function rewriteVersions(repository, records, idsMapping, logger) {
166
166
 
167
167
  record.snapshotId = newSnapshotId;
168
168
 
169
- const { id: recordId } = await repository.save(record); // eslint-disable-line no-await-in-loop
169
+ const { id: recordId } = await repository.save(record);
170
170
 
171
171
  if (recordId) {
172
172
  logger.info({ message: `Migrated version with new ID: ${recordId}`, serviceId: record.serviceId, type: record.termsType, id: record.id, current: i++, total: records.length });
@@ -42,7 +42,7 @@ let client;
42
42
  let counter = 1;
43
43
 
44
44
  for (const commit of filteredCommits.reverse()) { // reverse array to insert most recent commits first
45
- await collection.updateOne({ hash: commit.hash }, { $set: { ...commit } }, { upsert: true }); // eslint-disable-line no-await-in-loop
45
+ await collection.updateOne({ hash: commit.hash }, { $set: { ...commit } }, { upsert: true });
46
46
 
47
47
  if (counter % 1000 == 0) {
48
48
  logger.info({ message: ' ', current: counter, total: totalCommitToLoad });
@@ -48,14 +48,14 @@ async function removeDuplicateIssues() {
48
48
  continue;
49
49
  }
50
50
 
51
- await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', { /* eslint-disable-line no-await-in-loop */
51
+ await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
52
52
  owner,
53
53
  repo,
54
54
  issue_number: issue.number,
55
55
  state: 'closed',
56
56
  });
57
57
 
58
- await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', { /* eslint-disable-line no-await-in-loop */
58
+ await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
59
59
  owner,
60
60
  repo,
61
61
  issue_number: issue.number,
@@ -64,7 +64,6 @@ let recorder;
64
64
  const filteredCommits = commits.filter(({ message }) =>
65
65
  message.match(/^(Start tracking|Update)/));
66
66
 
67
- /* eslint-disable no-await-in-loop */
68
67
  /* eslint-disable no-continue */
69
68
  for (const commit of filteredCommits) {
70
69
  console.log(Date.now(), commit.hash, commit.date, commit.message);
@@ -74,7 +74,6 @@ let recorder;
74
74
  const filteredCommits = commits.filter(({ message }) =>
75
75
  message.match(/^(Start tracking|Update)/));
76
76
 
77
- /* eslint-disable no-await-in-loop */
78
77
  /* eslint-disable no-continue */
79
78
  for (const commit of filteredCommits) {
80
79
  console.log(Date.now(), commit.hash, commit.date, commit.message);
@@ -59,15 +59,12 @@ export async function extractFromHTML(sourceDocument) {
59
59
 
60
60
  for (const filterFunction of serviceSpecificFilters) {
61
61
  try {
62
- /* eslint-disable no-await-in-loop */
63
- // We want this to be made in series
64
62
  await filterFunction(webPageDOM, {
65
63
  fetch: location,
66
64
  select: contentSelectors,
67
65
  remove: insignificantContentSelectors,
68
66
  filter: serviceSpecificFilters.map(filter => filter.name),
69
67
  });
70
- /* eslint-enable no-await-in-loop */
71
68
  } catch (error) {
72
69
  throw new Error(`The filter function "${filterFunction.name}" failed: ${error}`);
73
70
  }
@@ -27,7 +27,7 @@ export const FETCHER_TYPES = {
27
27
  * @throws {FetchDocumentError} When the fetch operation fails
28
28
  * @async
29
29
  */
30
- export default async function fetch({
30
+ export default function fetch({
31
31
  url,
32
32
  executeClientScripts,
33
33
  cssSelectors,
@@ -187,7 +187,7 @@ export default class Archivist extends events.EventEmitter {
187
187
  const { location: url, executeClientScripts, cssSelectors } = sourceDocument;
188
188
 
189
189
  try {
190
- const { mimeType, content, fetcher } = await this.fetch({ url, executeClientScripts, cssSelectors }); // eslint-disable-line no-await-in-loop
190
+ const { mimeType, content, fetcher } = await this.fetch({ url, executeClientScripts, cssSelectors });
191
191
 
192
192
  sourceDocument.content = content;
193
193
  sourceDocument.mimeType = mimeType;
@@ -1382,7 +1382,6 @@ describe('GitRepository', () => {
1382
1382
 
1383
1383
  await subject.initialize();
1384
1384
 
1385
- /* eslint-disable no-await-in-loop */
1386
1385
  for (const commit of Object.values(commits)) {
1387
1386
  const { path: relativeFilePath, date, content, message } = commit;
1388
1387
  const filePath = path.join(RECORDER_PATH, relativeFilePath);
@@ -1396,7 +1395,6 @@ describe('GitRepository', () => {
1396
1395
  expectedIds.push(sha);
1397
1396
  expectedDates.push(date);
1398
1397
  }
1399
- /* eslint-enable no-await-in-loop */
1400
1398
  });
1401
1399
 
1402
1400
  after(() => subject.removeAll());
@@ -96,13 +96,11 @@ export default class MongoRepository extends RepositoryInterface {
96
96
  async* iterate() {
97
97
  const cursor = this.collection.find().sort({ fetchDate: 1 });
98
98
 
99
- /* eslint-disable no-await-in-loop */
100
99
  while (await cursor.hasNext()) {
101
100
  const mongoDocument = await cursor.next();
102
101
 
103
102
  yield this.#toDomain(mongoDocument);
104
103
  }
105
- /* eslint-enable no-await-in-loop */
106
104
  }
107
105
 
108
106
  removeAll() {
@@ -5,7 +5,7 @@ import Record from './record.js';
5
5
  export default class Version extends Record {
6
6
  static REQUIRED_PARAMS = Object.freeze([ ...Record.REQUIRED_PARAMS, 'snapshotIds' ]);
7
7
 
8
- static SOURCE_DOCUMENTS_SEPARATOR = '\n\n';
8
+ static SOURCE_DOCUMENTS_SEPARATOR = '\n\n- - -\n\n'; // Separator used to delimit source documents when concatenating them. The "- - -" produces a horizontal ruler in Markdown
9
9
 
10
10
  constructor(params) {
11
11
  super(params);
@@ -50,7 +50,7 @@ async function loadServiceFilters(serviceId, filterNames) {
50
50
  }
51
51
 
52
52
  const filterFilePath = `${serviceId}.filters.js`;
53
- const serviceFilters = await import(pathToFileURL(path.join(declarationsPath, filterFilePath))); // eslint-disable-line no-await-in-loop
53
+ const serviceFilters = await import(pathToFileURL(path.join(declarationsPath, filterFilePath)));
54
54
 
55
55
  return filterNames.map(filterName => serviceFilters[filterName]);
56
56
  }
@@ -74,7 +74,7 @@ async function loadServiceDocument(service, termsType, termsTypeDeclaration) {
74
74
  remove: sourceDocumentInsignificantContentSelectors,
75
75
  } = sourceDocument;
76
76
 
77
- const sourceDocumentFilters = await loadServiceFilters(service.id, sourceDocumentFilterNames); // eslint-disable-line no-await-in-loop
77
+ const sourceDocumentFilters = await loadServiceFilters(service.id, sourceDocumentFilterNames);
78
78
 
79
79
  sourceDocuments.push(new SourceDocument({
80
80
  location: sourceDocumentLocation || location,
@@ -101,7 +101,7 @@ export async function loadWithHistory(servicesIds = []) {
101
101
  const services = await load(servicesIds);
102
102
 
103
103
  for (const serviceId of Object.keys(services)) {
104
- const { declarations, filters } = await loadServiceHistoryFiles(serviceId); // eslint-disable-line no-await-in-loop
104
+ const { declarations, filters } = await loadServiceHistoryFiles(serviceId);
105
105
 
106
106
  for (const termsType of Object.keys(declarations)) {
107
107
  const termsTypeDeclarationEntries = declarations[termsType];
@@ -145,7 +145,7 @@ export async function loadWithHistory(servicesIds = []) {
145
145
  remove: sourceDocumentInsignificantContentSelectors,
146
146
  } = sourceDocument;
147
147
 
148
- const sourceDocumentFilters = await loadServiceFilters(serviceId, sourceDocumentFilterNames); // eslint-disable-line no-await-in-loop
148
+ const sourceDocumentFilters = await loadServiceFilters(serviceId, sourceDocumentFilterNames);
149
149
 
150
150
  sourceDocuments.push(new SourceDocument({
151
151
  location: sourceDocumentLocation || declarationForThisDate.fetch,
@@ -78,7 +78,7 @@ describe('Services', () => {
78
78
 
79
79
  for (let indexFilter = 0; indexFilter < expectedFilters.length; indexFilter++) {
80
80
  it(`has the proper "${expectedFilters[indexFilter].name}" filter function`, async () => {
81
- expect(await actualFilters[indexFilter]()).equal(await expectedFilters[indexFilter]()); // eslint-disable-line no-await-in-loop
81
+ expect(await actualFilters[indexFilter]()).equal(await expectedFilters[indexFilter]());
82
82
  });
83
83
  }
84
84
  } else {
@@ -220,7 +220,7 @@ describe('Services', () => {
220
220
 
221
221
  for (let indexFilter = 0; indexFilter < expectedFiltersForThisDate.length; indexFilter++) {
222
222
  it(`has the proper "${expectedFiltersForThisDate[indexFilter].name}" filter function`, async () => {
223
- expect(await actualFiltersForThisDate[indexFilter]()).equal(await expectedFiltersForThisDate[indexFilter]()); // eslint-disable-line no-await-in-loop
223
+ expect(await actualFiltersForThisDate[indexFilter]()).equal(await expectedFiltersForThisDate[indexFilter]());
224
224
  });
225
225
  }
226
226
  } else {
@@ -246,7 +246,7 @@ describe('Services', () => {
246
246
  indexFilter++
247
247
  ) {
248
248
  it(`has the proper "${expectedFilters[indexFilter].name}" filter function`, async () => {
249
- expect(await actualFilters[indexFilter]()).equal(await expectedFilters[indexFilter]()); // eslint-disable-line no-await-in-loop
249
+ expect(await actualFilters[indexFilter]()).equal(await expectedFilters[indexFilter]());
250
250
  });
251
251
  }
252
252
  } else {
@@ -51,26 +51,68 @@ export default class GitHub {
51
51
  async initialize() {
52
52
  this.MANAGED_LABELS = Object.values(LABELS);
53
53
  try {
54
- const existingLabels = await this.getRepositoryLabels();
54
+ let existingLabels = await this.getRepositoryLabels();
55
55
  const labelsToRemove = existingLabels.filter(label => label.description && label.description.includes(DEPRECATED_MANAGED_BY_OTA_MARKER));
56
56
 
57
57
  if (labelsToRemove.length) {
58
58
  logger.info(`Removing labels with deprecated markers: ${labelsToRemove.map(label => `"${label.name}"`).join(', ')}`);
59
59
 
60
60
  for (const label of labelsToRemove) {
61
- await this.deleteLabel(label.name); /* eslint-disable-line no-await-in-loop */
61
+ await this.deleteLabel(label.name);
62
62
  }
63
63
  }
64
64
 
65
- const updatedExistingLabels = labelsToRemove.length ? await this.getRepositoryLabels() : existingLabels; // Refresh labels after deletion, only if needed
66
- const existingLabelsNames = updatedExistingLabels.map(label => label.name);
65
+ if (labelsToRemove.length) {
66
+ existingLabels = await this.getRepositoryLabels();
67
+ }
68
+
69
+ const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
70
+ const obsoleteManagedLabels = existingLabels.filter(label => label.description?.includes(MANAGED_BY_OTA_MARKER) && !managedLabelsNames.includes(label.name));
71
+
72
+ if (obsoleteManagedLabels.length) {
73
+ logger.info(`Removing obsolete managed labels: ${obsoleteManagedLabels.map(label => `"${label.name}"`).join(', ')}`);
74
+
75
+ for (const label of obsoleteManagedLabels) {
76
+ await this.deleteLabel(label.name);
77
+ }
78
+ }
79
+
80
+ if (obsoleteManagedLabels.length) {
81
+ existingLabels = await this.getRepositoryLabels();
82
+ }
83
+
84
+ const existingLabelsNames = existingLabels.map(label => label.name);
67
85
  const missingLabels = this.MANAGED_LABELS.filter(label => !existingLabelsNames.includes(label.name));
68
86
 
69
87
  if (missingLabels.length) {
70
88
  logger.info(`Following required labels are not present on the repository: ${missingLabels.map(label => `"${label.name}"`).join(', ')}. Creating them…`);
71
89
 
72
90
  for (const label of missingLabels) {
73
- await this.createLabel({ /* eslint-disable-line no-await-in-loop */
91
+ await this.createLabel({
92
+ name: label.name,
93
+ color: label.color,
94
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
95
+ });
96
+ }
97
+ }
98
+
99
+ const labelsToUpdate = this.MANAGED_LABELS.filter(label => {
100
+ const existingLabel = existingLabels.find(existingLabel => existingLabel.name === label.name);
101
+
102
+ if (!existingLabel) {
103
+ return false;
104
+ }
105
+
106
+ const expectedDescription = `${label.description} ${MANAGED_BY_OTA_MARKER}`;
107
+
108
+ return existingLabel.description !== expectedDescription || existingLabel.color !== label.color;
109
+ });
110
+
111
+ if (labelsToUpdate.length) {
112
+ logger.info(`Updating labels with changed descriptions: ${labelsToUpdate.map(label => `"${label.name}"`).join(', ')}`);
113
+
114
+ for (const label of labelsToUpdate) {
115
+ await this.updateLabel({
74
116
  name: label.name,
75
117
  color: label.color,
76
118
  description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
@@ -134,6 +176,15 @@ export default class GitHub {
134
176
  });
135
177
  }
136
178
 
179
+ async updateLabel({ name, color, description }) {
180
+ await this.octokit.request('PATCH /repos/{owner}/{repo}/labels/{name}', {
181
+ ...this.commonParams,
182
+ name,
183
+ color,
184
+ description,
185
+ });
186
+ }
187
+
137
188
  async getIssue(title) {
138
189
  return (await this.issues).get(title);
139
190
  }
@@ -1,7 +1,7 @@
1
1
  import { expect } from 'chai';
2
2
  import nock from 'nock';
3
3
 
4
- import { LABELS } from '../labels.js';
4
+ import { LABELS, MANAGED_BY_OTA_MARKER } from '../labels.js';
5
5
 
6
6
  import GitHub from './index.js';
7
7
 
@@ -10,8 +10,8 @@ describe('GitHub', function () {
10
10
 
11
11
  let MANAGED_LABELS;
12
12
  let github;
13
- const EXISTING_OPEN_ISSUE = { number: 1, title: 'Opened issue', description: 'Issue description', state: GitHub.ISSUE_STATE_OPEN, labels: [{ name: 'page access restriction' }, { name: 'server error' }] };
14
- const EXISTING_CLOSED_ISSUE = { number: 2, title: 'Closed issue', description: 'Issue description', state: GitHub.ISSUE_STATE_CLOSED, labels: [{ name: 'empty content' }] };
13
+ const EXISTING_OPEN_ISSUE = { number: 1, title: 'Opened issue', description: 'Issue description', state: GitHub.ISSUE_STATE_OPEN, labels: [{ name: LABELS.HTTP_403.name }] };
14
+ const EXISTING_CLOSED_ISSUE = { number: 2, title: 'Closed issue', description: 'Issue description', state: GitHub.ISSUE_STATE_CLOSED, labels: [{ name: LABELS.EMPTY_CONTENT.name }] };
15
15
 
16
16
  before(async () => {
17
17
  MANAGED_LABELS = Object.values(LABELS);
@@ -24,31 +24,119 @@ describe('GitHub', function () {
24
24
  });
25
25
 
26
26
  describe('#initialize', () => {
27
- const scopes = [];
27
+ context('when some labels are missing', () => {
28
+ const scopes = [];
28
29
 
29
- before(async () => {
30
- const existingLabels = MANAGED_LABELS.slice(0, -2);
30
+ before(async () => {
31
+ const existingLabels = MANAGED_LABELS.slice(0, -2).map(label => ({
32
+ ...label,
33
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
34
+ }));
31
35
 
32
- nock('https://api.github.com')
33
- .get('/repos/owner/repo/labels')
34
- .query(true)
35
- .reply(200, existingLabels);
36
+ nock('https://api.github.com')
37
+ .get('/repos/owner/repo/labels')
38
+ .query(true)
39
+ .reply(200, existingLabels);
40
+
41
+ const missingLabels = MANAGED_LABELS.slice(-2);
36
42
 
37
- const missingLabels = MANAGED_LABELS.slice(-2);
43
+ for (const label of missingLabels) {
44
+ scopes.push(nock('https://api.github.com')
45
+ .post('/repos/owner/repo/labels', body => body.name === label.name)
46
+ .reply(200, label));
47
+ }
38
48
 
39
- for (const label of missingLabels) {
40
- scopes.push(nock('https://api.github.com')
41
- .post('/repos/owner/repo/labels', body => body.name === label.name)
42
- .reply(200, label));
43
- }
49
+ await github.initialize();
50
+ });
51
+
52
+ after(nock.cleanAll);
44
53
 
45
- await github.initialize();
54
+ it('should create missing labels', () => {
55
+ scopes.forEach(scope => expect(scope.isDone()).to.be.true);
56
+ });
46
57
  });
47
58
 
48
- after(nock.cleanAll);
59
+ context('when some labels are obsolete', () => {
60
+ const deleteScopes = [];
61
+
62
+ before(async () => {
63
+ const existingLabels = [
64
+ ...MANAGED_LABELS.map(label => ({
65
+ ...label,
66
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
67
+ })),
68
+ // Add an obsolete label that should be removed
69
+ {
70
+ name: 'obsolete label',
71
+ color: 'FF0000',
72
+ description: `This label is no longer used ${MANAGED_BY_OTA_MARKER}`,
73
+ },
74
+ ];
75
+
76
+ nock('https://api.github.com')
77
+ .get('/repos/owner/repo/labels')
78
+ .query(true)
79
+ .reply(200, existingLabels);
80
+
81
+ // Mock the delete call for the obsolete label
82
+ deleteScopes.push(nock('https://api.github.com')
83
+ .delete('/repos/owner/repo/labels/obsolete%20label')
84
+ .reply(200));
85
+
86
+ // Mock the second getRepositoryLabels call after deletion
87
+ nock('https://api.github.com')
88
+ .get('/repos/owner/repo/labels')
89
+ .query(true)
90
+ .reply(200, MANAGED_LABELS.map(label => ({
91
+ ...label,
92
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
93
+ })));
94
+
95
+ await github.initialize();
96
+ });
97
+
98
+ after(nock.cleanAll);
99
+
100
+ it('should remove obsolete managed labels', () => {
101
+ deleteScopes.forEach(scope => expect(scope.isDone()).to.be.true);
102
+ });
103
+ });
104
+
105
+ context('when some labels have changed descriptions', () => {
106
+ const updateScopes = [];
49
107
 
50
- it('should create missing labels', () => {
51
- scopes.forEach(scope => expect(scope.isDone()).to.be.true);
108
+ before(async () => {
109
+ const originalTestLabels = MANAGED_LABELS.slice(-2);
110
+ const testLabels = originalTestLabels.map(label => ({
111
+ ...label,
112
+ description: `${label.description} - obsolete description`,
113
+ }));
114
+
115
+ nock('https://api.github.com')
116
+ .persist()
117
+ .get('/repos/owner/repo/labels')
118
+ .query(true)
119
+ .reply(200, [ ...MANAGED_LABELS.slice(0, -2), ...testLabels ].map(label => ({
120
+ ...label,
121
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
122
+ })));
123
+
124
+ for (const label of originalTestLabels) {
125
+ updateScopes.push(nock('https://api.github.com')
126
+ .patch(`/repos/owner/repo/labels/${encodeURIComponent(label.name)}`, body =>
127
+ body.description === `${label.description} ${MANAGED_BY_OTA_MARKER}`)
128
+ .reply(200, label));
129
+ }
130
+ await github.initialize();
131
+ });
132
+
133
+ after(() => {
134
+ nock.cleanAll();
135
+ });
136
+
137
+ it('should update labels with changed descriptions', () => {
138
+ updateScopes.forEach(scope => expect(scope.isDone()).to.be.true);
139
+ });
52
140
  });
53
141
  });
54
142
 
@@ -280,7 +368,7 @@ describe('GitHub', function () {
280
368
  const ISSUE_TO_CREATE = {
281
369
  title: 'New Issue',
282
370
  description: 'Description of the new issue',
283
- labels: ['empty response'],
371
+ labels: [LABELS.EMPTY_RESPONSE.name],
284
372
  };
285
373
 
286
374
  before(async () => {
@@ -311,14 +399,14 @@ describe('GitHub', function () {
311
399
 
312
400
  before(async () => {
313
401
  updateIssueScope = nock('https://api.github.com')
314
- .patch(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: ['page access restriction'] })
402
+ .patch(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: [LABELS.HTTP_403.name] })
315
403
  .reply(200);
316
404
 
317
405
  addCommentScope = nock('https://api.github.com')
318
406
  .post(`/repos/owner/repo/issues/${EXISTING_CLOSED_ISSUE.number}/comments`, { body: EXISTING_CLOSED_ISSUE.description })
319
407
  .reply(200);
320
408
 
321
- await github.createOrUpdateIssue({ title: EXISTING_CLOSED_ISSUE.title, description: EXISTING_CLOSED_ISSUE.description, labels: ['page access restriction'] });
409
+ await github.createOrUpdateIssue({ title: EXISTING_CLOSED_ISSUE.title, description: EXISTING_CLOSED_ISSUE.description, labels: [LABELS.HTTP_403.name] });
322
410
  });
323
411
 
324
412
  after(() => {
@@ -342,14 +430,14 @@ describe('GitHub', function () {
342
430
 
343
431
  before(async () => {
344
432
  updateIssueScope = nock('https://api.github.com')
345
- .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: ['empty content'] })
433
+ .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: [LABELS.EMPTY_CONTENT.name] })
346
434
  .reply(200);
347
435
 
348
436
  addCommentScope = nock('https://api.github.com')
349
437
  .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`, { body: EXISTING_OPEN_ISSUE.description })
350
438
  .reply(200);
351
439
 
352
- await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: ['empty content'] });
440
+ await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: [LABELS.EMPTY_CONTENT.name] });
353
441
  });
354
442
 
355
443
  after(() => {
@@ -379,7 +467,7 @@ describe('GitHub', function () {
379
467
  .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`)
380
468
  .reply(200);
381
469
 
382
- await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: [ 'page access restriction', 'server error' ] });
470
+ await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: [LABELS.HTTP_403.name] });
383
471
  });
384
472
 
385
473
  after(() => {
@@ -403,14 +491,14 @@ describe('GitHub', function () {
403
491
 
404
492
  before(async () => {
405
493
  updateIssueScope = nock('https://api.github.com')
406
- .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: [ 'page access restriction', 'empty content' ] })
494
+ .patch(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}`, { state: GitHub.ISSUE_STATE_OPEN, labels: [ LABELS.HTTP_403.name, LABELS.NEEDS_INTERVENTION.name ] })
407
495
  .reply(200);
408
496
 
409
497
  addCommentScope = nock('https://api.github.com')
410
498
  .post(`/repos/owner/repo/issues/${EXISTING_OPEN_ISSUE.number}/comments`, { body: EXISTING_OPEN_ISSUE.description })
411
499
  .reply(200);
412
500
 
413
- await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: [ 'page access restriction', 'empty content' ] });
501
+ await github.createOrUpdateIssue({ title: EXISTING_OPEN_ISSUE.title, description: EXISTING_OPEN_ISSUE.description, labels: [ LABELS.HTTP_403.name, LABELS.NEEDS_INTERVENTION.name ] });
414
502
  });
415
503
 
416
504
  after(() => {
@@ -45,26 +45,69 @@ export default class GitLab {
45
45
 
46
46
  this.MANAGED_LABELS = Object.values(LABELS);
47
47
  try {
48
- const existingLabels = await this.getRepositoryLabels();
48
+ let existingLabels = await this.getRepositoryLabels();
49
49
  const labelsToRemove = existingLabels.filter(label => label.description && label.description.includes(DEPRECATED_MANAGED_BY_OTA_MARKER));
50
50
 
51
51
  if (labelsToRemove.length) {
52
52
  logger.info(`Removing labels with deprecated markers: ${labelsToRemove.map(label => `"${label.name}"`).join(', ')}`);
53
53
 
54
54
  for (const label of labelsToRemove) {
55
- await this.deleteLabel(label.name); /* eslint-disable-line no-await-in-loop */
55
+ await this.deleteLabel(label.name);
56
56
  }
57
57
  }
58
58
 
59
- const updatedExistingLabels = labelsToRemove.length ? await this.getRepositoryLabels() : existingLabels; // Refresh labels after deletion, only if needed
60
- const existingLabelsNames = updatedExistingLabels.map(label => label.name);
59
+ if (labelsToRemove.length) {
60
+ existingLabels = await this.getRepositoryLabels();
61
+ }
62
+
63
+ const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
64
+ const obsoleteManagedLabels = existingLabels.filter(label => label.description?.includes(MANAGED_BY_OTA_MARKER) && !managedLabelsNames.includes(label.name));
65
+
66
+ if (obsoleteManagedLabels.length) {
67
+ logger.info(`Removing obsolete managed labels: ${obsoleteManagedLabels.map(label => `"${label.name}"`).join(', ')}`);
68
+
69
+ for (const label of obsoleteManagedLabels) {
70
+ await this.deleteLabel(label.name);
71
+ }
72
+ }
73
+
74
+ if (obsoleteManagedLabels.length) {
75
+ existingLabels = await this.getRepositoryLabels();
76
+ }
77
+
78
+ const existingLabelsNames = existingLabels.map(label => label.name);
61
79
  const missingLabels = this.MANAGED_LABELS.filter(label => !existingLabelsNames.includes(label.name));
62
80
 
63
81
  if (missingLabels.length) {
64
82
  logger.info(`Following required labels are not present on the repository: ${missingLabels.map(label => `"${label.name}"`).join(', ')}. Creating them…`);
65
83
 
66
84
  for (const label of missingLabels) {
67
- await this.createLabel({ /* eslint-disable-line no-await-in-loop */
85
+ await this.createLabel({
86
+ name: label.name,
87
+ color: `#${label.color}`,
88
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
89
+ });
90
+ }
91
+ }
92
+
93
+ const labelsToUpdate = this.MANAGED_LABELS.filter(label => {
94
+ const existingLabel = existingLabels.find(existingLabel => existingLabel.name === label.name);
95
+
96
+ if (!existingLabel) {
97
+ return false;
98
+ }
99
+
100
+ const expectedDescription = `${label.description} ${MANAGED_BY_OTA_MARKER}`;
101
+ const expectedColor = `#${label.color}`;
102
+
103
+ return existingLabel.description !== expectedDescription || existingLabel.color !== expectedColor;
104
+ });
105
+
106
+ if (labelsToUpdate.length) {
107
+ logger.info(`Updating labels with changed descriptions: ${labelsToUpdate.map(label => `"${label.name}"`).join(', ')}`);
108
+
109
+ for (const label of labelsToUpdate) {
110
+ await this.updateLabel({
68
111
  name: label.name,
69
112
  color: `#${label.color}`,
70
113
  description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
@@ -155,6 +198,40 @@ export default class GitLab {
155
198
  }
156
199
  }
157
200
 
201
+ async updateLabel({ name, color, description }) {
202
+ try {
203
+ const label = {
204
+ name,
205
+ color,
206
+ description,
207
+ };
208
+
209
+ const options = GitLab.baseOptionsHttpReq();
210
+
211
+ options.method = 'PUT';
212
+ options.body = JSON.stringify(label);
213
+ options.headers = {
214
+ 'Content-Type': 'application/json',
215
+ ...options.headers,
216
+ };
217
+
218
+ const response = await nodeFetch(
219
+ `${this.apiBaseURL}/projects/${this.projectId}/labels/${encodeURIComponent(name)}`,
220
+ options,
221
+ );
222
+
223
+ const res = await response.json();
224
+
225
+ if (response.ok) {
226
+ logger.info(`Label updated: ${res.name}, color: ${res.color}`);
227
+ } else {
228
+ logger.error(`updateLabel response: ${JSON.stringify(res)}`);
229
+ }
230
+ } catch (error) {
231
+ logger.error(`Failed to update label: ${error}`);
232
+ }
233
+ }
234
+
158
235
  async createIssue({ title, description, labels }) {
159
236
  try {
160
237
  const issue = {
@@ -1,7 +1,7 @@
1
1
  import { expect } from 'chai';
2
2
  import nock from 'nock';
3
3
 
4
- import { LABELS } from '../labels.js';
4
+ import { LABELS, MANAGED_BY_OTA_MARKER } from '../labels.js';
5
5
 
6
6
  import GitLab from './index.js';
7
7
 
@@ -18,34 +18,134 @@ describe('GitLab', function () {
18
18
  });
19
19
 
20
20
  describe('#initialize', () => {
21
- const scopes = [];
21
+ context('when some labels are missing', () => {
22
+ const scopes = [];
22
23
 
23
- before(async () => {
24
- const existingLabels = MANAGED_LABELS.slice(0, -2);
24
+ before(async () => {
25
+ const existingLabels = MANAGED_LABELS.slice(0, -2).map(label => ({
26
+ ...label,
27
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
28
+ }));
25
29
 
26
- nock(gitlab.apiBaseURL)
27
- .get(`/projects/${encodeURIComponent('owner/repo')}`)
28
- .reply(200, { id: PROJECT_ID });
30
+ nock(gitlab.apiBaseURL)
31
+ .get(`/projects/${encodeURIComponent('owner/repo')}`)
32
+ .reply(200, { id: PROJECT_ID });
29
33
 
30
- nock(gitlab.apiBaseURL)
31
- .get(`/projects/${PROJECT_ID}/labels?with_counts=true`)
32
- .reply(200, existingLabels);
34
+ nock(gitlab.apiBaseURL)
35
+ .get(`/projects/${PROJECT_ID}/labels?with_counts=true`)
36
+ .reply(200, existingLabels);
33
37
 
34
- const missingLabels = MANAGED_LABELS.slice(-2);
38
+ const missingLabels = MANAGED_LABELS.slice(-2);
35
39
 
36
- for (const label of missingLabels) {
37
- scopes.push(nock(gitlab.apiBaseURL)
38
- .post(`/projects/${PROJECT_ID}/labels`)
39
- .reply(200, { name: label.name }));
40
- }
40
+ for (const label of missingLabels) {
41
+ scopes.push(nock(gitlab.apiBaseURL)
42
+ .post(`/projects/${PROJECT_ID}/labels`)
43
+ .reply(200, { name: label.name }));
44
+ }
41
45
 
42
- await gitlab.initialize();
46
+ await gitlab.initialize();
47
+ });
48
+
49
+ after(nock.cleanAll);
50
+
51
+ it('should create missing labels', () => {
52
+ scopes.forEach(scope => expect(scope.isDone()).to.be.true);
53
+ });
43
54
  });
44
55
 
45
- after(nock.cleanAll);
56
+ context('when some labels are obsolete', () => {
57
+ const deleteScopes = [];
58
+
59
+ before(async () => {
60
+ const existingLabels = [
61
+ ...MANAGED_LABELS.map(label => ({
62
+ ...label,
63
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
64
+ })),
65
+ // Add an obsolete label that should be removed
66
+ {
67
+ name: 'obsolete label',
68
+ color: '#FF0000',
69
+ description: `This label is no longer used ${MANAGED_BY_OTA_MARKER}`,
70
+ },
71
+ ];
72
+
73
+ nock(gitlab.apiBaseURL)
74
+ .get(`/projects/${encodeURIComponent('owner/repo')}`)
75
+ .reply(200, { id: PROJECT_ID });
76
+
77
+ nock(gitlab.apiBaseURL)
78
+ .get(`/projects/${PROJECT_ID}/labels?with_counts=true`)
79
+ .reply(200, existingLabels);
46
80
 
47
- it('should create missing labels', () => {
48
- scopes.forEach(scope => expect(scope.isDone()).to.be.true);
81
+ // Mock the delete call for the obsolete label
82
+ deleteScopes.push(nock(gitlab.apiBaseURL)
83
+ .delete(`/projects/${PROJECT_ID}/labels/${encodeURIComponent('obsolete label')}`)
84
+ .reply(200));
85
+
86
+ // Mock the second getRepositoryLabels call after deletion
87
+ nock(gitlab.apiBaseURL)
88
+ .get(`/projects/${PROJECT_ID}/labels?with_counts=true`)
89
+ .reply(200, MANAGED_LABELS.map(label => ({
90
+ ...label,
91
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
92
+ })));
93
+
94
+ await gitlab.initialize();
95
+ });
96
+
97
+ after(nock.cleanAll);
98
+
99
+ it('should remove obsolete managed labels', () => {
100
+ deleteScopes.forEach(scope => expect(scope.isDone()).to.be.true);
101
+ });
102
+ });
103
+
104
+ context('when some labels have changed descriptions', () => {
105
+ const updateScopes = [];
106
+
107
+ before(async () => {
108
+ const originalTestLabels = MANAGED_LABELS.slice(-2);
109
+ const testLabels = originalTestLabels.map(label => ({
110
+ ...label,
111
+ description: `${label.description} - obsolete description`,
112
+ }));
113
+
114
+ nock(gitlab.apiBaseURL)
115
+ .get(`/projects/${encodeURIComponent('owner/repo')}`)
116
+ .reply(200, { id: PROJECT_ID });
117
+
118
+ nock(gitlab.apiBaseURL)
119
+ .persist()
120
+ .get(`/projects/${PROJECT_ID}/labels?with_counts=true`)
121
+ .reply(200, [
122
+ ...MANAGED_LABELS.slice(0, -2).map(label => ({
123
+ ...label,
124
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
125
+ })),
126
+ ...testLabels.map(label => ({
127
+ ...label,
128
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
129
+ })),
130
+ ]);
131
+
132
+ for (const label of originalTestLabels) {
133
+ updateScopes.push(nock(gitlab.apiBaseURL)
134
+ .put(`/projects/${PROJECT_ID}/labels/${encodeURIComponent(label.name)}`, body =>
135
+ body.description === `${label.description} ${MANAGED_BY_OTA_MARKER}`)
136
+ .reply(200, { name: label.name, color: `#${label.color}` }));
137
+ }
138
+
139
+ await gitlab.initialize();
140
+ });
141
+
142
+ after(() => {
143
+ nock.cleanAll();
144
+ });
145
+
146
+ it('should update labels with changed descriptions', () => {
147
+ updateScopes.forEach(scope => expect(scope.isDone()).to.be.true);
148
+ });
49
149
  });
50
150
  });
51
151
 
@@ -405,7 +505,7 @@ describe('GitLab', function () {
405
505
  const ISSUE_TO_CREATE = {
406
506
  title: 'New Issue',
407
507
  description: 'Description of the new issue',
408
- labels: ['empty response'],
508
+ labels: [LABELS.EMPTY_RESPONSE.name],
409
509
  };
410
510
 
411
511
  before(async () => {
@@ -436,7 +536,7 @@ describe('GitLab', function () {
436
536
  const ISSUE = {
437
537
  title: 'Existing Issue',
438
538
  description: 'New comment',
439
- labels: ['page access restriction'],
539
+ labels: [LABELS.HTTP_403.name],
440
540
  };
441
541
 
442
542
  context('when issue is closed', () => {
@@ -448,7 +548,7 @@ describe('GitLab', function () {
448
548
  iid: 123,
449
549
  title: ISSUE.title,
450
550
  description: ISSUE.description,
451
- labels: [{ name: 'empty content' }],
551
+ labels: [{ name: LABELS.EMPTY_CONTENT.name }],
452
552
  state: GitLab.ISSUE_STATE_CLOSED,
453
553
  };
454
554
 
@@ -456,7 +556,7 @@ describe('GitLab', function () {
456
556
  const responseIssuereopened = { iid: 123 };
457
557
  const responseSetLabels = {
458
558
  iid: 123,
459
- labels: ['page access restriction'],
559
+ labels: [LABELS.HTTP_403.name],
460
560
  };
461
561
  const responseAddcomment = { iid: 123, id: 23, body: ISSUE.description };
462
562
  const { iid } = GITLAB_RESPONSE_FOR_EXISTING_ISSUE;
@@ -471,7 +571,7 @@ describe('GitLab', function () {
471
571
  .reply(200, responseIssuereopened);
472
572
 
473
573
  setIssueLabelsScope = nock(gitlab.apiBaseURL)
474
- .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: ['page access restriction'] })
574
+ .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: [LABELS.HTTP_403.name] })
475
575
  .reply(200, responseSetLabels);
476
576
 
477
577
  addCommentScope = nock(gitlab.apiBaseURL)
@@ -504,7 +604,7 @@ describe('GitLab', function () {
504
604
  iid: 123,
505
605
  title: ISSUE.title,
506
606
  description: ISSUE.description,
507
- labels: [{ name: 'empty content' }],
607
+ labels: [{ name: LABELS.EMPTY_CONTENT.name }],
508
608
  state: GitLab.ISSUE_STATE_OPEN,
509
609
  };
510
610
 
@@ -512,7 +612,7 @@ describe('GitLab', function () {
512
612
  const responseIssuereopened = { iid: 123 };
513
613
  const responseSetLabels = {
514
614
  iid: 123,
515
- labels: ['page access restriction'],
615
+ labels: [LABELS.HTTP_403.name],
516
616
  };
517
617
  const responseAddcomment = { iid: 123, id: 23, body: ISSUE.description };
518
618
  const { iid } = GITLAB_RESPONSE_FOR_EXISTING_ISSUE;
@@ -527,7 +627,7 @@ describe('GitLab', function () {
527
627
  .reply(200, responseIssuereopened);
528
628
 
529
629
  setIssueLabelsScope = nock(gitlab.apiBaseURL)
530
- .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: ['page access restriction'] })
630
+ .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: [LABELS.HTTP_403.name] })
531
631
  .reply(200, responseSetLabels);
532
632
 
533
633
  addCommentScope = nock(gitlab.apiBaseURL)
@@ -559,7 +659,7 @@ describe('GitLab', function () {
559
659
  iid: 123,
560
660
  title: ISSUE.title,
561
661
  description: ISSUE.description,
562
- labels: [{ name: 'page access restriction' }, { name: 'server error' }],
662
+ labels: [{ name: LABELS.HTTP_403.name }],
563
663
  state: GitLab.ISSUE_STATE_OPEN,
564
664
  };
565
665
 
@@ -585,7 +685,7 @@ describe('GitLab', function () {
585
685
  await gitlab.createOrUpdateIssue({
586
686
  title: ISSUE.title,
587
687
  description: ISSUE.description,
588
- labels: [ 'page access restriction', 'server error' ],
688
+ labels: [LABELS.HTTP_403.name],
589
689
  });
590
690
  });
591
691
 
@@ -615,7 +715,7 @@ describe('GitLab', function () {
615
715
  iid: 123,
616
716
  title: ISSUE.title,
617
717
  description: ISSUE.description,
618
- labels: [{ name: 'page access restriction' }],
718
+ labels: [{ name: LABELS.HTTP_403.name }],
619
719
  state: GitLab.ISSUE_STATE_OPEN,
620
720
  };
621
721
 
@@ -623,7 +723,7 @@ describe('GitLab', function () {
623
723
  const responseIssuereopened = { iid: 123 };
624
724
  const responseSetLabels = {
625
725
  iid: 123,
626
- labels: [ 'page access restriction', 'empty content' ],
726
+ labels: [ LABELS.HTTP_403.name, LABELS.EMPTY_CONTENT.name ],
627
727
  };
628
728
  const responseAddcomment = { iid: 123, id: 23, body: ISSUE.description };
629
729
  const { iid } = GITLAB_RESPONSE_FOR_EXISTING_ISSUE;
@@ -638,7 +738,7 @@ describe('GitLab', function () {
638
738
  .reply(200, responseIssuereopened);
639
739
 
640
740
  setIssueLabelsScope = nock(gitlab.apiBaseURL)
641
- .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: [ 'page access restriction', 'empty content' ] })
741
+ .put(`/projects/${PROJECT_ID}/issues/${iid}`, { labels: [ LABELS.HTTP_403.name, LABELS.EMPTY_CONTENT.name ] })
642
742
  .reply(200, responseSetLabels);
643
743
 
644
744
  addCommentScope = nock(gitlab.apiBaseURL)
@@ -648,7 +748,7 @@ describe('GitLab', function () {
648
748
  await gitlab.createOrUpdateIssue({
649
749
  title: ISSUE.title,
650
750
  description: ISSUE.description,
651
- labels: [ 'page access restriction', 'empty content' ],
751
+ labels: [ LABELS.HTTP_403.name, LABELS.EMPTY_CONTENT.name ],
652
752
  });
653
753
  });
654
754
 
@@ -10,21 +10,21 @@ const CONTRIBUTION_TOOL_URL = 'https://contribute.opentermsarchive.org/en/servic
10
10
  const DOC_URL = 'https://docs.opentermsarchive.org';
11
11
 
12
12
  const ERROR_MESSAGE_TO_ISSUE_LABELS_MAP = {
13
- 'has no match': [ LABELS.PAGE_STRUCTURE_CHANGE.name, LABELS.NEEDS_INTERVENTION.name ],
14
- 'HTTP code 404': [ LABELS.PAGE_NOT_FOUND.name, LABELS.NEEDS_INTERVENTION.name ],
13
+ 'has no match': [ LABELS.DOCUMENT_STRUCTURE_CHANGE.name, LABELS.NEEDS_INTERVENTION.name ],
14
+ 'HTTP code 404': [ LABELS.DOCUMENT_NOT_FOUND.name, LABELS.NEEDS_INTERVENTION.name ],
15
15
  'HTTP code 403': [LABELS.HTTP_403.name],
16
16
  'HTTP code 429': [LABELS.HTTP_429.name],
17
17
  'HTTP code 500': [LABELS.HTTP_500.name],
18
18
  'HTTP code 502': [LABELS.HTTP_502.name],
19
19
  'HTTP code 503': [LABELS.HTTP_503.name],
20
- 'Timed out after': [LABELS.PAGE_LOAD_TIMEOUT.name],
20
+ 'Timed out after': [LABELS.DOCUMENT_LOAD_TIMEOUT.name],
21
21
  EAI_AGAIN: [LABELS.DNS_LOOKUP_FAILURE.name],
22
22
  ENOTFOUND: [LABELS.DNS_RESOLUTION_FAILURE.name],
23
23
  'Response is empty': [LABELS.EMPTY_RESPONSE.name],
24
24
  'unable to verify the first certificate': [LABELS.SSL_INVALID.name],
25
25
  'certificate has expired': [LABELS.SSL_EXPIRED.name],
26
26
  'maximum redirect reached': [LABELS.TOO_MANY_REDIRECTS.name],
27
- 'not a valid selector': [LABELS.INVALID_SELECTOR.name],
27
+ 'not a valid selector': [ LABELS.INVALID_SELECTOR.name, LABELS.NEEDS_INTERVENTION.name ],
28
28
  'empty content': [LABELS.EMPTY_CONTENT.name],
29
29
  };
30
30
 
@@ -3,90 +3,90 @@ export const DEPRECATED_MANAGED_BY_OTA_MARKER = '[managed by OTA]';
3
3
  export const MANAGED_BY_OTA_MARKER = '- Auto-managed by OTA engine';
4
4
 
5
5
  export const LABELS = {
6
- HTTP_403: {
7
- name: 'page access restriction',
6
+ DNS_LOOKUP_FAILURE: {
7
+ name: 'domain lookup failed',
8
8
  color: 'FFFFFF',
9
- description: 'Fetching failed with a 403 (forbidden) HTTP code',
9
+ description: 'Fetching failed because the request to DNS servers timed out',
10
10
  },
11
- HTTP_429: {
12
- name: 'request limit exceeded',
11
+ DNS_RESOLUTION_FAILURE: {
12
+ name: 'domain resolution failed',
13
13
  color: 'FFFFFF',
14
- description: 'Fetching failed with a 429 (too many requests) HTTP code',
14
+ description: 'Fetching failed because the domain name failed to resolve',
15
15
  },
16
- HTTP_500: {
17
- name: 'server error',
16
+ DOCUMENT_LOAD_TIMEOUT: {
17
+ name: 'document load timed out',
18
18
  color: 'FFFFFF',
19
- description: 'Fetching failed with a 500 (internal server error) HTTP code',
19
+ description: 'Fetching failed with a timeout error',
20
20
  },
21
- HTTP_502: {
22
- name: 'server response failure',
21
+ DOCUMENT_NOT_FOUND: {
22
+ name: 'document not found',
23
23
  color: 'FFFFFF',
24
- description: 'Fetching failed with a 502 (bad gateway) HTTP code',
24
+ description: 'Fetch location is outdated',
25
25
  },
26
- HTTP_503: {
27
- name: 'server unavailability',
26
+ DOCUMENT_STRUCTURE_CHANGE: {
27
+ name: 'document structure changed',
28
28
  color: 'FFFFFF',
29
- description: 'Fetching failed with a 503 (service unavailable) HTTP code',
29
+ description: 'Selectors do not match any content in the document',
30
30
  },
31
- SSL_EXPIRED: {
32
- name: 'SSL certificate expiration',
31
+ EMPTY_CONTENT: {
32
+ name: 'document was empty',
33
33
  color: 'FFFFFF',
34
- description: 'Fetching failed because the domain SSL certificate has expired',
34
+ description: 'Fetching failed because the server returns an empty content',
35
35
  },
36
- DNS_LOOKUP_FAILURE: {
37
- name: 'DNS lookup failure',
36
+ EMPTY_RESPONSE: {
37
+ name: 'received empty response',
38
38
  color: 'FFFFFF',
39
- description: 'Fetching failed because the domain temporarily failed to resolve on DNS',
39
+ description: 'Fetching failed because server returned an empty response body',
40
40
  },
41
- DNS_RESOLUTION_FAILURE: {
42
- name: 'DNS resolution failure',
41
+ HTTP_403: {
42
+ name: 'document access forbidden',
43
43
  color: 'FFFFFF',
44
- description: 'Fetching failed because the domain fails to resolve on DNS',
44
+ description: 'Fetching failed with a 403 (forbidden) HTTP code',
45
45
  },
46
- EMPTY_RESPONSE: {
47
- name: 'empty response',
46
+ HTTP_429: {
47
+ name: 'access limit exceeded',
48
48
  color: 'FFFFFF',
49
- description: 'Fetching failed because server returned an empty response body',
49
+ description: 'Fetching failed with a 429 (too many requests) HTTP code',
50
50
  },
51
- SSL_INVALID: {
52
- name: 'SSL certificate invalidity',
51
+ HTTP_500: {
52
+ name: 'server errored',
53
53
  color: 'FFFFFF',
54
- description: 'Fetching failed because SSL certificate verification failed',
54
+ description: 'Fetching failed with a 500 (internal server error) HTTP code',
55
55
  },
56
- TOO_MANY_REDIRECTS: {
57
- name: 'too many redirects',
56
+ HTTP_502: {
57
+ name: 'upstream server errored',
58
58
  color: 'FFFFFF',
59
- description: 'Fetching failed because too many redirects detected',
59
+ description: 'Fetching failed with a 502 (bad gateway) HTTP code',
60
60
  },
61
- PAGE_LOAD_TIMEOUT: {
62
- name: 'page load timeout',
61
+ HTTP_503: {
62
+ name: 'server was unavailable',
63
63
  color: 'FFFFFF',
64
- description: 'Fetching failed with a timeout error',
64
+ description: 'Fetching failed with a 503 (service unavailable) HTTP code',
65
65
  },
66
- EMPTY_CONTENT: {
67
- name: 'empty content',
66
+ INVALID_SELECTOR: {
67
+ name: 'invalid selector',
68
68
  color: 'FFFFFF',
69
- description: 'Fetching failed because the server returns an empty content',
69
+ description: 'Some selectors cannot be understood by the engine',
70
70
  },
71
- UNKNOWN_FAILURE: {
72
- name: 'unknown failure reason',
71
+ SSL_EXPIRED: {
72
+ name: 'certificate expired',
73
73
  color: 'FFFFFF',
74
- description: 'Fetching failed for an undetermined reason that needs investigation',
74
+ description: 'Fetching failed because the domain SSL certificate has expired',
75
75
  },
76
- PAGE_STRUCTURE_CHANGE: {
77
- name: 'page structure change',
76
+ SSL_INVALID: {
77
+ name: 'certificate was invalid',
78
78
  color: 'FFFFFF',
79
- description: 'Extraction selectors are outdated',
79
+ description: 'Fetching failed because the domain SSL certificate failed to verify',
80
80
  },
81
- PAGE_NOT_FOUND: {
82
- name: 'page not found',
81
+ TOO_MANY_REDIRECTS: {
82
+ name: 'redirected too many times',
83
83
  color: 'FFFFFF',
84
- description: 'Fetch location is outdated',
84
+ description: 'Fetching failed because of too many redirects',
85
85
  },
86
- INVALID_SELECTOR: {
87
- name: 'invalid selector',
86
+ UNKNOWN_FAILURE: {
87
+ name: 'failed for an unknown reason',
88
88
  color: 'FFFFFF',
89
- description: 'Some selectors cannot be understood by the engine',
89
+ description: 'Fetching failed for an undetermined reason that needs investigation',
90
90
  },
91
91
  NEEDS_INTERVENTION: {
92
92
  name: '⚠ needs intervention',