@opentermsarchive/engine 0.31.0 → 0.32.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 +6 -0
- package/config/default.json +5 -6
- package/package.json +1 -1
- package/src/archivist/errors.js +5 -5
- package/src/archivist/index.js +1 -0
- package/src/index.js +4 -4
- package/src/reporter/github.js +206 -0
- package/src/reporter/index.js +144 -0
- package/src/reporter/labels.json +77 -0
- package/src/reporter/labels.test.js +30 -0
- package/src/tracker/index.js +0 -215
- /package/src/{tracker → reporter}/README.md +0 -0
package/.eslintrc.yaml
CHANGED
|
@@ -65,6 +65,12 @@ rules:
|
|
|
65
65
|
- error
|
|
66
66
|
- argsIgnorePattern: next
|
|
67
67
|
no-use-before-define: 0
|
|
68
|
+
lines-between-class-members:
|
|
69
|
+
- error
|
|
70
|
+
- enforce:
|
|
71
|
+
- blankLine: always
|
|
72
|
+
prev: method
|
|
73
|
+
next: method
|
|
68
74
|
padding-line-between-statements:
|
|
69
75
|
- error
|
|
70
76
|
- blankLine: always
|
package/config/default.json
CHANGED
|
@@ -54,13 +54,12 @@
|
|
|
54
54
|
"updateTemplateId": 7
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"
|
|
57
|
+
"reporter": {
|
|
58
58
|
"githubIssues": {
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"description": "Automatically created when terms cannot be tracked"
|
|
59
|
+
"repositories": {
|
|
60
|
+
"declarations": "OpenTermsArchive/sandbox-declarations",
|
|
61
|
+
"versions": "OpenTermsArchive/sandbox-versions",
|
|
62
|
+
"snapshots": "OpenTermsArchive/sandbox-snapshots"
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
},
|
package/package.json
CHANGED
package/src/archivist/errors.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export class InaccessibleContentError extends Error {
|
|
2
|
-
constructor(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
super(`The documents cannot be accessed or their contents can not be selected:${message}`);
|
|
2
|
+
constructor(reasonOrReasons) {
|
|
3
|
+
const reasons = [].concat(reasonOrReasons);
|
|
4
|
+
|
|
5
|
+
super(`The documents cannot be accessed or their contents can not be selected:${`\n - ${reasons.join('\n - ')}`}`);
|
|
7
6
|
this.name = 'InaccessibleContentError';
|
|
7
|
+
this.reasons = reasons;
|
|
8
8
|
}
|
|
9
9
|
}
|
package/src/archivist/index.js
CHANGED
|
@@ -179,6 +179,7 @@ export default class Archivist extends events.EventEmitter {
|
|
|
179
179
|
|
|
180
180
|
sourceDocument.content = snapshot.content;
|
|
181
181
|
sourceDocument.mimeType = snapshot.mimeType;
|
|
182
|
+
sourceDocument.snapshotId = snapshot.id;
|
|
182
183
|
terms.fetchDate = snapshot.fetchDate;
|
|
183
184
|
}));
|
|
184
185
|
}
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import cron from 'croner';
|
|
|
4
4
|
import Archivist from './archivist/index.js';
|
|
5
5
|
import logger from './logger/index.js';
|
|
6
6
|
import Notifier from './notifier/index.js';
|
|
7
|
-
import
|
|
7
|
+
import Reporter from './reporter/index.js';
|
|
8
8
|
|
|
9
9
|
export default async function track({ services, types, extractOnly, schedule }) {
|
|
10
10
|
const archivist = new Archivist({
|
|
@@ -43,10 +43,10 @@ export default async function track({ services, types, extractOnly, schedule })
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (process.env.GITHUB_TOKEN) {
|
|
46
|
-
const
|
|
46
|
+
const reporter = new Reporter(config.get('reporter'));
|
|
47
47
|
|
|
48
|
-
await
|
|
49
|
-
archivist.attach(
|
|
48
|
+
await reporter.initialize();
|
|
49
|
+
archivist.attach(reporter);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (!schedule) {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
import { Octokit } from 'octokit';
|
|
4
|
+
|
|
5
|
+
import logger from '../logger/index.js';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export const MANAGED_BY_OTA_MARKER = '[managed by OTA]';
|
|
10
|
+
|
|
11
|
+
export default class GitHub {
|
|
12
|
+
static ISSUE_STATE_CLOSED = 'closed';
|
|
13
|
+
static ISSUE_STATE_OPEN = 'open';
|
|
14
|
+
static ISSUE_STATE_ALL = 'all';
|
|
15
|
+
|
|
16
|
+
constructor(repository) {
|
|
17
|
+
const { version } = require('../../package.json');
|
|
18
|
+
|
|
19
|
+
this.octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, userAgent: `opentermsarchive/${version}` });
|
|
20
|
+
|
|
21
|
+
const [ owner, repo ] = repository.split('/');
|
|
22
|
+
|
|
23
|
+
this.commonParams = { owner, repo };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async initialize() {
|
|
27
|
+
try {
|
|
28
|
+
const { data: user } = await this.octokit.request('GET /user', { ...this.commonParams });
|
|
29
|
+
|
|
30
|
+
this.authenticatedUserLogin = user.login;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error(`🤖 Could not get authenticated user: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.MANAGED_LABELS = require('./labels.json');
|
|
36
|
+
|
|
37
|
+
const existingLabels = await this.getRepositoryLabels();
|
|
38
|
+
const existingLabelsNames = existingLabels.map(label => label.name);
|
|
39
|
+
const missingLabels = this.MANAGED_LABELS.filter(label => !existingLabelsNames.includes(label.name));
|
|
40
|
+
|
|
41
|
+
if (missingLabels.length) {
|
|
42
|
+
logger.info(`🤖 Following required labels are not present on the repository: ${missingLabels.map(label => `"${label.name}"`).join(', ')}. Creating them…`);
|
|
43
|
+
|
|
44
|
+
for (const label of missingLabels) {
|
|
45
|
+
await this.createLabel({ /* eslint-disable-line no-await-in-loop */
|
|
46
|
+
name: label.name,
|
|
47
|
+
color: label.color,
|
|
48
|
+
description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getRepositoryLabels() {
|
|
55
|
+
try {
|
|
56
|
+
const { data: labels } = await this.octokit.request('GET /repos/{owner}/{repo}/labels', { ...this.commonParams });
|
|
57
|
+
|
|
58
|
+
return labels;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.error(`🤖 Could get labels: ${error}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async createLabel({ name, color, description }) {
|
|
65
|
+
try {
|
|
66
|
+
await this.octokit.request('POST /repos/{owner}/{repo}/labels', {
|
|
67
|
+
...this.commonParams,
|
|
68
|
+
name,
|
|
69
|
+
color,
|
|
70
|
+
description,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
logger.info(`🤖 Created repository label "${name}"`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error(`🤖 Could not create label "${name}": ${error}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async createIssue({ title, description: body, labels }) {
|
|
80
|
+
try {
|
|
81
|
+
const { data: issue } = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
|
|
82
|
+
...this.commonParams,
|
|
83
|
+
title,
|
|
84
|
+
body,
|
|
85
|
+
labels,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
logger.info(`🤖 Created GitHub issue #${issue.number} "${title}": ${issue.html_url}`);
|
|
89
|
+
|
|
90
|
+
return issue;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(`🤖 Could not create GitHub issue "${title}": ${error}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async setIssueLabels({ issue, labels }) {
|
|
97
|
+
try {
|
|
98
|
+
await this.octokit.request('PUT /repos/{owner}/{repo}/issues/{issue_number}/labels', {
|
|
99
|
+
...this.commonParams,
|
|
100
|
+
issue_number: issue.number,
|
|
101
|
+
labels,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
logger.info(`🤖 Updated labels to GitHub issue #${issue.number}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger.error(`🤖 Could not update GitHub issue #${issue.number} "${issue.title}": ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async openIssue(issue) {
|
|
111
|
+
try {
|
|
112
|
+
await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
|
|
113
|
+
...this.commonParams,
|
|
114
|
+
issue_number: issue.number,
|
|
115
|
+
state: GitHub.ISSUE_STATE_OPEN,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
logger.info(`🤖 Opened GitHub issue #${issue.number}`);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error(`🤖 Could not update GitHub issue #${issue.number} "${issue.title}": ${error}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async closeIssue(issue) {
|
|
125
|
+
try {
|
|
126
|
+
await this.octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', {
|
|
127
|
+
...this.commonParams,
|
|
128
|
+
issue_number: issue.number,
|
|
129
|
+
state: GitHub.ISSUE_STATE_CLOSED,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
logger.info(`🤖 Closed GitHub issue #${issue.number}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error(`🤖 Could not update GitHub issue #${issue.number} "${issue.title}": ${error}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getIssue({ title, ...searchParams }) {
|
|
139
|
+
try {
|
|
140
|
+
const issues = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
|
|
141
|
+
...this.commonParams,
|
|
142
|
+
per_page: 100,
|
|
143
|
+
creator: this.authenticatedUserLogin,
|
|
144
|
+
...searchParams,
|
|
145
|
+
}, response => response.data);
|
|
146
|
+
|
|
147
|
+
const [issue] = issues.filter(item => item.title === title); // since only one is expected, use the first one
|
|
148
|
+
|
|
149
|
+
return issue;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.error(`🤖 Could not find GitHub issue "${title}": ${error}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async addCommentToIssue({ issue, comment: body }) {
|
|
156
|
+
try {
|
|
157
|
+
const { data: comment } = await this.octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
|
158
|
+
...this.commonParams,
|
|
159
|
+
issue_number: issue.number,
|
|
160
|
+
body,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
logger.info(`🤖 Added comment to GitHub issue #${issue.number}: ${comment.html_url}`);
|
|
164
|
+
|
|
165
|
+
return comment;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.error(`🤖 Could not add comment to GitHub issue #${issue.number} "${issue.title}": ${error}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async closeIssueWithCommentIfExists({ title, comment }) {
|
|
172
|
+
const openedIssue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_OPEN });
|
|
173
|
+
|
|
174
|
+
if (!openedIssue) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.addCommentToIssue({ issue: openedIssue, comment });
|
|
179
|
+
|
|
180
|
+
return this.closeIssue(openedIssue);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async createOrUpdateIssue({ title, description, label }) {
|
|
184
|
+
const issue = await this.getIssue({ title, state: GitHub.ISSUE_STATE_ALL });
|
|
185
|
+
|
|
186
|
+
if (!issue) {
|
|
187
|
+
return this.createIssue({ title, description, labels: [label] });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (issue.state == this.ISSUE_STATE_CLOSED) {
|
|
191
|
+
await this.openIssue(issue);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
|
|
195
|
+
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
|
|
196
|
+
|
|
197
|
+
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
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const labelsNotManagedToKeep = issue.labels.map(label => label.name).filter(label => !managedLabelsNames.includes(label));
|
|
202
|
+
|
|
203
|
+
await this.setIssueLabels({ issue, labels: [ label, ...labelsNotManagedToKeep ] });
|
|
204
|
+
await this.addCommentToIssue({ issue, comment: description });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import mime from 'mime';
|
|
2
|
+
|
|
3
|
+
import GitHub from './github.js';
|
|
4
|
+
|
|
5
|
+
const CONTRIBUTION_TOOL_URL = 'https://contribute.opentermsarchive.org/en/service';
|
|
6
|
+
const DOC_URL = 'https://docs.opentermsarchive.org';
|
|
7
|
+
|
|
8
|
+
const ERROR_MESSAGE_TO_ISSUE_LABEL_MAP = {
|
|
9
|
+
'has no match': 'selectors',
|
|
10
|
+
'HTTP code 404': 'location',
|
|
11
|
+
'HTTP code 403': '403',
|
|
12
|
+
'HTTP code 429': '429',
|
|
13
|
+
'HTTP code 500': '500',
|
|
14
|
+
'HTTP code 502': '502',
|
|
15
|
+
'HTTP code 503': '503',
|
|
16
|
+
'Timed out after': 'timeout',
|
|
17
|
+
'getaddrinfo EAI_AGAIN': 'EAI_AGAIN',
|
|
18
|
+
'getaddrinfo ENOTFOUND': 'ENOTFOUND',
|
|
19
|
+
'Response is empty': 'empty response',
|
|
20
|
+
'unable to verify the first certificate': 'first certificate',
|
|
21
|
+
'certificate has expired': 'certificate expired',
|
|
22
|
+
'maximum redirect reached': 'redirects',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getLabelNameFromError(error) {
|
|
26
|
+
return ERROR_MESSAGE_TO_ISSUE_LABEL_MAP[Object.keys(ERROR_MESSAGE_TO_ISSUE_LABEL_MAP).find(substring => error.toString().includes(substring))] || 'to clarify';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// In the following class, it is assumed that each issue is managed using its title as a unique identifier
|
|
30
|
+
export default class Reporter {
|
|
31
|
+
constructor(config) {
|
|
32
|
+
const { repositories } = config.githubIssues;
|
|
33
|
+
|
|
34
|
+
for (const repositoryType of Object.keys(repositories)) {
|
|
35
|
+
if (!repositories[repositoryType].includes('/') || repositories[repositoryType].includes('https://')) {
|
|
36
|
+
throw new Error(`Configuration entry "reporter.githubIssues.repositories.${repositoryType}" is expected to be a string in the format <owner>/<repo>, but received: "${repositories[repositoryType]}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.github = new GitHub(repositories.declarations);
|
|
41
|
+
this.repositories = repositories;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async initialize() {
|
|
45
|
+
return this.github.initialize();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async onVersionRecorded(version) {
|
|
49
|
+
await this.github.closeIssueWithCommentIfExists({
|
|
50
|
+
title: Reporter.generateTitleID(version.serviceId, version.termsType),
|
|
51
|
+
comment: `### Tracking resumed
|
|
52
|
+
|
|
53
|
+
A new version has been recorded.`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async onVersionNotChanged(version) {
|
|
58
|
+
await this.github.closeIssueWithCommentIfExists({
|
|
59
|
+
title: Reporter.generateTitleID(version.serviceId, version.termsType),
|
|
60
|
+
comment: `### Tracking resumed
|
|
61
|
+
|
|
62
|
+
No changes were found in the last run, so no new version has been recorded.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async onFirstVersionRecorded(version) {
|
|
67
|
+
return this.onVersionRecorded(version);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async onInaccessibleContent(error, terms) {
|
|
71
|
+
await this.github.createOrUpdateIssue({
|
|
72
|
+
title: Reporter.generateTitleID(terms.service.id, terms.type),
|
|
73
|
+
description: this.generateDescription({ error, terms }),
|
|
74
|
+
label: getLabelNameFromError(error),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
generateDescription({ error, terms }) {
|
|
79
|
+
const date = new Date();
|
|
80
|
+
const currentFormattedDate = date.toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short', timeZone: 'UTC' });
|
|
81
|
+
const validUntil = date.toISOString().replace(/\.\d+/, ''); // ISO date without milliseconds
|
|
82
|
+
|
|
83
|
+
const hasSnapshots = terms.sourceDocuments.every(sourceDocument => sourceDocument.snapshotId);
|
|
84
|
+
|
|
85
|
+
const contributionToolParams = new URLSearchParams({
|
|
86
|
+
json: JSON.stringify(terms.toPersistence()),
|
|
87
|
+
destination: this.repositories.declarations,
|
|
88
|
+
step: '2',
|
|
89
|
+
});
|
|
90
|
+
const contributionToolUrl = `${CONTRIBUTION_TOOL_URL}?${contributionToolParams}`;
|
|
91
|
+
|
|
92
|
+
const latestDeclarationLink = `[Latest declaration](https://github.com/${this.repositories.declarations}/blob/main/declarations/${encodeURIComponent(terms.service.name)}.json)`;
|
|
93
|
+
const latestVersionLink = `[Latest version](https://github.com/${this.repositories.versions}/blob/main/${encodeURIComponent(terms.service.name)}/${encodeURIComponent(terms.type)}.md)`;
|
|
94
|
+
const snapshotsBaseUrl = `https://github.com/${this.repositories.snapshots}/blob/main/${encodeURIComponent(terms.service.name)}/${encodeURIComponent(terms.type)}`;
|
|
95
|
+
const latestSnapshotsLink = terms.hasMultipleSourceDocuments
|
|
96
|
+
? `Latest snapshots:\n - ${terms.sourceDocuments.map(sourceDocument => `[${sourceDocument.id}](${snapshotsBaseUrl}.%20#${sourceDocument.id}.${mime.getExtension(sourceDocument.mimeType)})`).join('\n - ')}`
|
|
97
|
+
: `[Latest snapshot](${snapshotsBaseUrl}.${mime.getExtension(terms.sourceDocuments[0].mimeType)})`;
|
|
98
|
+
|
|
99
|
+
/* eslint-disable no-irregular-whitespace */
|
|
100
|
+
return `
|
|
101
|
+
### No version of the \`${terms.type}\` of service \`${terms.service.name}\` is recorded anymore since ${currentFormattedDate}
|
|
102
|
+
|
|
103
|
+
The source document${terms.hasMultipleSourceDocuments ? 's have' : ' has'}${hasSnapshots ? ' ' : ' not '}been recorded in ${terms.hasMultipleSourceDocuments ? 'snapshots' : 'a snapshot'}, ${hasSnapshots ? 'but ' : 'thus '} no version can be [extracted](${DOC_URL}/#tracking-terms).
|
|
104
|
+
${hasSnapshots ? 'After correction, it might still be possible to recover the missed versions.' : ''}
|
|
105
|
+
|
|
106
|
+
### What went wrong
|
|
107
|
+
|
|
108
|
+
- ${error.reasons.join('\n- ')}
|
|
109
|
+
|
|
110
|
+
### How to resume tracking
|
|
111
|
+
|
|
112
|
+
First of all, check if the source documents are accessible through a web browser:
|
|
113
|
+
|
|
114
|
+
- [ ] ${terms.sourceDocuments.map(sourceDocument => `[${sourceDocument.location}](${sourceDocument.location})`).join('\n- [ ] ')}
|
|
115
|
+
|
|
116
|
+
#### If the source documents are accessible through a web browser
|
|
117
|
+
|
|
118
|
+
[Edit the declaration](${contributionToolUrl}):
|
|
119
|
+
- Try updating the selectors.
|
|
120
|
+
- Try switching client scripts on with expert mode.
|
|
121
|
+
|
|
122
|
+
#### If the source documents are not accessible anymore
|
|
123
|
+
|
|
124
|
+
- If the source documents have moved, find their new location and [update it](${contributionToolUrl}).
|
|
125
|
+
- If these terms have been removed, move them from the declaration to its [history file](${DOC_URL}/contributing-terms/#service-history), using \`${validUntil}\` as the \`validUntil\` value.
|
|
126
|
+
- If the service has closed, move the entire contents of the declaration to its [history file](${DOC_URL}/contributing-terms/#service-history), using \`${validUntil}\` as the \`validUntil\` value.
|
|
127
|
+
|
|
128
|
+
#### If none of the above works
|
|
129
|
+
|
|
130
|
+
If the source documents are accessible in a browser but fetching them always fails from the Open Terms Archive server, this is most likely because the service provider has blocked the Open Terms Archive robots from accessing its content. In this case, updating the declaration will not enable resuming tracking. Only an agreement with the service provider, an engine upgrade, or some technical workarounds provided by the administrator of this collection’s server might resume tracking.
|
|
131
|
+
|
|
132
|
+
### References
|
|
133
|
+
|
|
134
|
+
- ${latestDeclarationLink}
|
|
135
|
+
- ${latestVersionLink}
|
|
136
|
+
- ${latestSnapshotsLink}
|
|
137
|
+
`;
|
|
138
|
+
/* eslint-enable no-irregular-whitespace */
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static generateTitleID(serviceId, type) {
|
|
142
|
+
return `\`${serviceId}\` ‧ \`${type}\` ‧ not tracked anymore`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "403",
|
|
4
|
+
"color": "0b08a0",
|
|
5
|
+
"description": "Fetching fails with a 403 (forbidden) HTTP code"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "429",
|
|
9
|
+
"color": "0b08a0",
|
|
10
|
+
"description": "Fetching fails with a 429 (too many requests) HTTP code"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "500",
|
|
14
|
+
"color": "0b08a0",
|
|
15
|
+
"description": "Fetching fails with a 500 (internal server error) HTTP code"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "502",
|
|
19
|
+
"color": "0b08a0",
|
|
20
|
+
"description": "Fetching fails with a 502 (bad gateway) HTTP code"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "503",
|
|
24
|
+
"color": "0b08a0",
|
|
25
|
+
"description": "Fetching fails with a 503 (service unavailable) HTTP code"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "certificate expired",
|
|
29
|
+
"color": "0b08a0",
|
|
30
|
+
"description": "Fetching fails because the domain SSL certificate has expired"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "EAI_AGAIN",
|
|
34
|
+
"color": "0b08a0",
|
|
35
|
+
"description": "Fetching fails because the domain fails to resolve on DNS"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "ENOTFOUND",
|
|
39
|
+
"color": "0b08a0",
|
|
40
|
+
"description": "Fetching fails because the domain fails to resolve on DNS"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "empty response",
|
|
44
|
+
"color": "0b08a0",
|
|
45
|
+
"description": "Fetching fails with a “response is empty” error"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "first certificate",
|
|
49
|
+
"color": "0b08a0",
|
|
50
|
+
"description": "Fetching fails with an “unable to verify the first certificate” error"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "redirects",
|
|
54
|
+
"color": "0b08a0",
|
|
55
|
+
"description": "Fetching fails with a “too many redirects” error"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "timeout",
|
|
59
|
+
"color": "0b08a0",
|
|
60
|
+
"description": "Fetching fails with a timeout error"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "to clarify",
|
|
64
|
+
"color": "0496ff",
|
|
65
|
+
"description": "Default failure label"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "selectors",
|
|
69
|
+
"color": "FBCA04",
|
|
70
|
+
"description": "Extraction selectors are outdated"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "location",
|
|
74
|
+
"color": "FBCA04",
|
|
75
|
+
"description": "Fetch location is outdated"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
import chai from 'chai';
|
|
4
|
+
|
|
5
|
+
import { MANAGED_BY_OTA_MARKER } from './github.js';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
const { expect } = chai;
|
|
10
|
+
const labels = require('./labels.json');
|
|
11
|
+
|
|
12
|
+
const GITHUB_LABEL_DESCRIPTION_MAX_LENGTH = 100;
|
|
13
|
+
|
|
14
|
+
describe('Reporter GitHub labels', () => {
|
|
15
|
+
labels.forEach(label => {
|
|
16
|
+
describe(`"${label.name}"`, () => {
|
|
17
|
+
it('complies with the GitHub character limit for descriptions', () => {
|
|
18
|
+
const descriptionLength = label.description.length + MANAGED_BY_OTA_MARKER.length;
|
|
19
|
+
|
|
20
|
+
expect(descriptionLength).to.be.lessThan(GITHUB_LABEL_DESCRIPTION_MAX_LENGTH);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('complies with the GitHub constraints for color', () => {
|
|
24
|
+
const validHexColorRegex = /^[0-9a-fA-F]{6}$/; // Regex for a valid 6-digit hexadecimal color code without the `#`
|
|
25
|
+
|
|
26
|
+
expect(validHexColorRegex.test(label.color)).to.be.true;
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
package/src/tracker/index.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
import { Octokit } from 'octokit';
|
|
4
|
-
|
|
5
|
-
import logger from '../logger/index.js';
|
|
6
|
-
|
|
7
|
-
const { version } = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url)).toString());
|
|
8
|
-
|
|
9
|
-
const ISSUE_STATE_CLOSED = 'closed';
|
|
10
|
-
const ISSUE_STATE_OPEN = 'open';
|
|
11
|
-
const ISSUE_STATE_ALL = 'all';
|
|
12
|
-
|
|
13
|
-
const CONTRIBUTE_URL = 'https://contribute.opentermsarchive.org/en/service';
|
|
14
|
-
const GOOGLE_URL = 'https://www.google.com/search?q=';
|
|
15
|
-
|
|
16
|
-
export default class Tracker {
|
|
17
|
-
static isRepositoryValid(repository) {
|
|
18
|
-
return repository.includes('/');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
constructor(trackerConfig) {
|
|
22
|
-
const { repository, label } = trackerConfig.githubIssues;
|
|
23
|
-
|
|
24
|
-
if (!Tracker.isRepositoryValid(repository)) {
|
|
25
|
-
throw new Error('tracker.githubIssues.repository should be a string with <owner>/<repo>');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const [ owner, repo ] = repository.split('/');
|
|
29
|
-
|
|
30
|
-
this.octokit = new Octokit({
|
|
31
|
-
auth: process.env.GITHUB_TOKEN,
|
|
32
|
-
userAgent: `opentermsarchive/${version}`,
|
|
33
|
-
});
|
|
34
|
-
this.cachedIssues = {};
|
|
35
|
-
this.commonParams = {
|
|
36
|
-
owner,
|
|
37
|
-
repo,
|
|
38
|
-
accept: 'application/vnd.github.v3+json',
|
|
39
|
-
};
|
|
40
|
-
this.repository = repository;
|
|
41
|
-
this.label = label;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async initialize() {
|
|
45
|
-
await this.createLabel({
|
|
46
|
-
name: this.label.name,
|
|
47
|
-
color: this.label.color,
|
|
48
|
-
description: this.label.description,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async onVersionRecorded(serviceId, type) {
|
|
53
|
-
await this.closeIssueIfExists({
|
|
54
|
-
labels: [this.label.name],
|
|
55
|
-
title: `Fix ${serviceId} - ${type}`,
|
|
56
|
-
comment: '🤖 Closed automatically as data was gathered successfully',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async onVersionNotChanged(serviceId, type) {
|
|
61
|
-
await this.closeIssueIfExists({
|
|
62
|
-
labels: [this.label.name],
|
|
63
|
-
title: `Fix ${serviceId} - ${type}`,
|
|
64
|
-
comment: '🤖 Closed automatically as version is unchanged but data has been fetched correctly',
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async onFirstVersionRecorded(serviceId, type) {
|
|
69
|
-
return this.onVersionRecorded(serviceId, type);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async onInaccessibleContent(error, terms) {
|
|
73
|
-
const { title, body } = Tracker.formatIssueTitleAndBody({ message: error.toString(), repository: this.repository, terms });
|
|
74
|
-
|
|
75
|
-
await this.createIssueIfNotExists({
|
|
76
|
-
title,
|
|
77
|
-
body,
|
|
78
|
-
labels: [this.label.name],
|
|
79
|
-
comment: '🤖 Reopened automatically as an error occured',
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async createLabel(params) {
|
|
84
|
-
return this.octokit.rest.issues.createLabel({ ...this.commonParams, ...params })
|
|
85
|
-
.catch(error => {
|
|
86
|
-
if (error.toString().includes('"code":"already_exists"')) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
logger.error(`Could not create label "${params.name}": ${error.toString()}`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async createIssue(params) {
|
|
94
|
-
const { data } = await this.octokit.rest.issues.create(params);
|
|
95
|
-
|
|
96
|
-
return data;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async searchIssues({ title, ...searchParams }) {
|
|
100
|
-
const request = {
|
|
101
|
-
per_page: 100,
|
|
102
|
-
...searchParams,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const issues = await this.octokit.paginate(
|
|
106
|
-
this.octokit.rest.issues.listForRepo,
|
|
107
|
-
request,
|
|
108
|
-
response => response.data,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const issuesWithSameTitle = issues.filter(item => item.title === title);
|
|
112
|
-
|
|
113
|
-
return issuesWithSameTitle;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async addCommentToIssue(params) {
|
|
117
|
-
const { data } = await this.octokit.rest.issues.createComment(params);
|
|
118
|
-
|
|
119
|
-
return data;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async createIssueIfNotExists({ title, body, labels, comment }) {
|
|
123
|
-
try {
|
|
124
|
-
const existingIssues = await this.searchIssues({ ...this.commonParams, title, labels, state: ISSUE_STATE_ALL });
|
|
125
|
-
|
|
126
|
-
if (!existingIssues.length) {
|
|
127
|
-
const existingIssue = await this.createIssue({ ...this.commonParams, title, body, labels });
|
|
128
|
-
|
|
129
|
-
logger.info(`🤖 Creating GitHub issue for ${title}: ${existingIssue.html_url}`);
|
|
130
|
-
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const openedIssues = existingIssues.filter(existingIssue => existingIssue.state === ISSUE_STATE_OPEN);
|
|
135
|
-
const hasNoneOpened = openedIssues.length === 0;
|
|
136
|
-
|
|
137
|
-
for (const existingIssue of existingIssues) {
|
|
138
|
-
if (hasNoneOpened) {
|
|
139
|
-
try {
|
|
140
|
-
/* eslint-disable no-await-in-loop */
|
|
141
|
-
await this.octokit.rest.issues.update({
|
|
142
|
-
...this.commonParams,
|
|
143
|
-
issue_number: existingIssue.number,
|
|
144
|
-
state: ISSUE_STATE_OPEN,
|
|
145
|
-
});
|
|
146
|
-
await this.addCommentToIssue({
|
|
147
|
-
...this.commonParams,
|
|
148
|
-
issue_number: existingIssue.number,
|
|
149
|
-
body: `${comment}\n${body}`,
|
|
150
|
-
});
|
|
151
|
-
/* eslint-enable no-await-in-loop */
|
|
152
|
-
logger.info(`🤖 Reopened automatically as an error occured for ${title}: ${existingIssue.html_url}`);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
logger.error(`🤖 Could not update GitHub issue ${existingIssue.html_url}: ${e}`);
|
|
155
|
-
}
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
} catch (e) {
|
|
160
|
-
logger.error(`🤖 Could not create GitHub issue for ${title}: ${e}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async closeIssueIfExists({ title, comment, labels }) {
|
|
165
|
-
try {
|
|
166
|
-
const openedIssues = await this.searchIssues({ ...this.commonParams, title, labels, state: ISSUE_STATE_OPEN });
|
|
167
|
-
|
|
168
|
-
for (const openedIssue of openedIssues) {
|
|
169
|
-
try {
|
|
170
|
-
await this.octokit.rest.issues.update({ ...this.commonParams, issue_number: openedIssue.number, state: ISSUE_STATE_CLOSED }); // eslint-disable-line no-await-in-loop
|
|
171
|
-
await this.addCommentToIssue({ ...this.commonParams, issue_number: openedIssue.number, body: comment }); // eslint-disable-line no-await-in-loop
|
|
172
|
-
logger.info(`🤖 GitHub issue closed for ${title}: ${openedIssue.html_url}`);
|
|
173
|
-
} catch (e) {
|
|
174
|
-
logger.error(`🤖 Could not close GitHub issue ${openedIssue.html_url}: ${e.toString()}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch (e) {
|
|
178
|
-
logger.error(`🤖 Could not close GitHub issue for ${title}: ${e}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
static formatIssueTitleAndBody({ message, repository, terms }) {
|
|
183
|
-
const { service: { name }, type } = terms;
|
|
184
|
-
const json = terms.toPersistence();
|
|
185
|
-
const title = `Fix ${name} - ${type}`;
|
|
186
|
-
|
|
187
|
-
const encodedName = encodeURIComponent(name);
|
|
188
|
-
const encodedType = encodeURIComponent(type);
|
|
189
|
-
|
|
190
|
-
const urlQueryParams = new URLSearchParams({
|
|
191
|
-
json: JSON.stringify(json),
|
|
192
|
-
destination: repository,
|
|
193
|
-
expertMode: 'true',
|
|
194
|
-
step: '2',
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const body = `
|
|
198
|
-
These terms are no longer tracked.
|
|
199
|
-
|
|
200
|
-
${message}
|
|
201
|
-
|
|
202
|
-
Check what's wrong by:
|
|
203
|
-
- Using the [online contribution tool](${CONTRIBUTE_URL}?${urlQueryParams}).
|
|
204
|
-
${message.includes('404') ? `- [Searching Google](${GOOGLE_URL}%22${encodedName}%22+%22${encodedType}%22) to get for a new URL.` : ''}
|
|
205
|
-
|
|
206
|
-
And some info about what has already been tracked:
|
|
207
|
-
- See [service declaration JSON file](https://github.com/${repository}/blob/main/declarations/${encodedName}.json).
|
|
208
|
-
`;
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
title,
|
|
212
|
-
body,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
}
|
|
File without changes
|