@opentermsarchive/engine 2.5.0 → 2.7.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.
@@ -0,0 +1,386 @@
1
+ import { createRequire } from 'module';
2
+
3
+ import HttpProxyAgent from 'http-proxy-agent';
4
+ import HttpsProxyAgent from 'https-proxy-agent';
5
+ import nodeFetch from 'node-fetch';
6
+
7
+ import logger from '../../logger/index.js';
8
+
9
+ const require = createRequire(import.meta.url);
10
+
11
+ export const MANAGED_BY_OTA_MARKER = '[managed by OTA]';
12
+ const BASE_URL = 'https://gitlab.com';
13
+ const API_BASE_URL = 'https://gitlab.com/api/v4';
14
+
15
+ export default class GitLab {
16
+ static ISSUE_STATE_CLOSED = 'closed';
17
+ static ISSUE_STATE_OPEN = 'opened';
18
+ static ISSUE_STATE_ALL = 'all';
19
+
20
+ constructor(repository, baseURL = BASE_URL, apiBaseURL = API_BASE_URL) {
21
+ const [ owner, repo ] = repository.split('/');
22
+
23
+ this.commonParams = { owner, repo };
24
+ this.projectId = null;
25
+ this.baseURL = baseURL;
26
+ console.log('this.baseURL', this.baseURL);
27
+ this.apiBaseURL = apiBaseURL;
28
+ }
29
+
30
+ async initialize() {
31
+ const options = GitLab.baseOptionsHttpReq();
32
+
33
+ try {
34
+ const repositoryPath = `${this.commonParams.owner}/${this.commonParams.repo}`;
35
+ const response = await nodeFetch(
36
+ `${this.apiBaseURL}/projects/${encodeURIComponent(repositoryPath)}`,
37
+ options,
38
+ );
39
+
40
+ const res = await response.json();
41
+
42
+ if (response.ok) {
43
+ this.projectId = res.id;
44
+ } else {
45
+ logger.error(`Error while obtaining projectId: ${JSON.strinfigy(res)}`);
46
+ this.projectId = null;
47
+ }
48
+ } catch (error) {
49
+ logger.error(`Error while obtaining projectId: ${error}`);
50
+ this.projectId = null;
51
+ }
52
+
53
+ this.MANAGED_LABELS = require('./labels.json');
54
+
55
+ const existingLabels = await this.getRepositoryLabels();
56
+ const existingLabelsNames = existingLabels.map(label => label.name);
57
+ const missingLabels = this.MANAGED_LABELS.filter(label => !existingLabelsNames.includes(label.name));
58
+
59
+ if (missingLabels.length) {
60
+ logger.info(`The following required labels are not present on the repository: ${missingLabels.map(label => `"${label.name}"`).join(', ')}. Creating them…`);
61
+
62
+ for (const label of missingLabels) {
63
+ await this.createLabel({ /* eslint-disable-line no-await-in-loop */
64
+ name: label.name,
65
+ color: label.color,
66
+ description: `${label.description} ${MANAGED_BY_OTA_MARKER}`,
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ async getRepositoryLabels() {
73
+ try {
74
+ const options = GitLab.baseOptionsHttpReq();
75
+ const response = await nodeFetch(
76
+ `${this.apiBaseURL}/projects/${this.projectId}/labels?with_counts=true`,
77
+ options,
78
+ );
79
+
80
+ const res = await response.json();
81
+
82
+ if (response.ok) {
83
+ return res;
84
+ }
85
+
86
+ logger.error(`Failed to get labels: ${response.status} - ${JSON.stringify(res)}`);
87
+
88
+ return null;
89
+ } catch (error) {
90
+ logger.error(`Could not get labels: ${error}`);
91
+ }
92
+ }
93
+
94
+ async createLabel({ name, color, description }) {
95
+ try {
96
+ const label = {
97
+ name,
98
+ color,
99
+ description,
100
+ };
101
+
102
+ const options = GitLab.baseOptionsHttpReq();
103
+
104
+ options.method = 'POST';
105
+ options.body = JSON.stringify(label);
106
+ options.headers = {
107
+ 'Content-Type': 'application/json',
108
+ ...options.headers,
109
+ };
110
+
111
+ const response = await nodeFetch(
112
+ `${this.apiBaseURL}/projects/${this.projectId}/labels`,
113
+ options,
114
+ );
115
+
116
+ const res = await response.json();
117
+
118
+ if (response.ok) {
119
+ logger.info(`New label created: ${res.name} , color: ${res.color}`);
120
+ } else {
121
+ logger.error(`createLabel response: ${JSON.stringify(res)}`);
122
+ }
123
+ } catch (error) {
124
+ logger.error(`Failed to create label: ${error}`);
125
+ }
126
+ }
127
+
128
+ async createIssue({ title, description, labels }) {
129
+ try {
130
+ const issue = {
131
+ title,
132
+ labels,
133
+ description,
134
+ };
135
+
136
+ const options = GitLab.baseOptionsHttpReq();
137
+
138
+ options.method = 'POST';
139
+ options.body = JSON.stringify(issue);
140
+ options.headers = {
141
+ 'Content-Type': 'application/json',
142
+ ...options.headers,
143
+ };
144
+
145
+ const response = await nodeFetch(
146
+ `${this.apiBaseURL}/projects/${this.projectId}/issues`,
147
+ options,
148
+ );
149
+
150
+ const res = await response.json();
151
+
152
+ if (response.ok) {
153
+ logger.info(`Created GitLab issue #${res.iid} "${title}": ${res.web_url}`);
154
+
155
+ return res;
156
+ }
157
+
158
+ logger.error(`createIssue response: ${JSON.stringify(res)}`);
159
+ } catch (error) {
160
+ logger.error(`Could not create GitLab issue "${title}": ${error}`);
161
+ }
162
+ }
163
+
164
+ async setIssueLabels({ issue, labels }) {
165
+ const newLabels = { labels };
166
+ const options = GitLab.baseOptionsHttpReq();
167
+
168
+ options.method = 'PUT';
169
+ options.body = JSON.stringify(newLabels);
170
+ options.headers = {
171
+ 'Content-Type': 'application/json',
172
+ ...options.headers,
173
+ };
174
+
175
+ try {
176
+ const response = await nodeFetch(
177
+ `${this.apiBaseURL}/projects/${this.projectId}/issues/${issue.iid}`,
178
+ options,
179
+ );
180
+
181
+ const res = await response.json();
182
+
183
+ if (response.ok) {
184
+ logger.info(`Updated labels to GitLab issue #${issue.iid}`);
185
+ } else {
186
+ logger.error(`setIssueLabels response: ${JSON.stringify(res)}`);
187
+ }
188
+ } catch (error) {
189
+ logger.error(`Could not update GitLab issue #${issue.iid} "${issue.title}": ${error}`);
190
+ }
191
+ }
192
+
193
+ async openIssue(issue) {
194
+ const updateIssue = { state_event: 'reopen' };
195
+ const options = GitLab.baseOptionsHttpReq();
196
+
197
+ options.method = 'PUT';
198
+ options.body = JSON.stringify(updateIssue);
199
+ options.headers = {
200
+ 'Content-Type': 'application/json',
201
+ ...options.headers,
202
+ };
203
+
204
+ try {
205
+ const response = await nodeFetch(
206
+ `${this.apiBaseURL}/projects/${this.projectId}/issues/${issue.iid}`,
207
+ options,
208
+ );
209
+ const res = await response.json();
210
+
211
+ if (response.ok) {
212
+ logger.info(`Opened GitLab issue #${res.iid}`);
213
+ } else {
214
+ logger.error(`openIssue response: ${JSON.stringify(res)}`);
215
+ }
216
+ } catch (error) {
217
+ logger.error(`Could not update GitLab issue #${issue.iid} "${issue.title}": ${error}`);
218
+ }
219
+ }
220
+
221
+ async closeIssue(issue) {
222
+ const updateIssue = { state_event: 'close' };
223
+
224
+ const options = GitLab.baseOptionsHttpReq();
225
+
226
+ options.method = 'PUT';
227
+ options.body = JSON.stringify(updateIssue);
228
+ options.headers = {
229
+ 'Content-Type': 'application/json',
230
+ ...options.headers,
231
+ };
232
+
233
+ try {
234
+ const response = await nodeFetch(
235
+ `${this.apiBaseURL}/projects/${this.projectId}/issues/${issue.iid}`,
236
+ options,
237
+ );
238
+ const res = await response.json();
239
+
240
+ if (response.ok) {
241
+ logger.info(`Closed GitLab issue #${issue.iid}`);
242
+ } else {
243
+ logger.error(`closeIssue response: ${JSON.stringify(res)}`);
244
+ }
245
+ } catch (error) {
246
+ logger.error(`Could not update GitLab issue #${issue.iid} "${issue.title}": ${error}`);
247
+ }
248
+ }
249
+
250
+ async getIssue({ title, ...searchParams }) {
251
+ try {
252
+ let apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?state=${searchParams.state}&per_page=100`;
253
+
254
+ if (searchParams.state == 'all') apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?per_page=100`;
255
+ apiUrl = `${this.apiBaseURL}/projects/${this.projectId}/issues?search=${encodeURIComponent(title)}&per_page=100`;
256
+
257
+ const options = GitLab.baseOptionsHttpReq();
258
+
259
+ options.method = 'GET';
260
+
261
+ const response = await nodeFetch(apiUrl, options);
262
+ const res = await response.json();
263
+
264
+ if (response.ok) {
265
+ const issues = res;
266
+
267
+ const [issue] = issues.filter(item => item.title === title); // since only one is expected, use the first one
268
+
269
+ return issue;
270
+ }
271
+
272
+ logger.error(`openIssue response: ${JSON.stringify(res)}`);
273
+ } catch (error) {
274
+ logger.error(`Could not find GitLab issue "${title}": ${error}`);
275
+ }
276
+ }
277
+
278
+ async addCommentToIssue({ issue, comment }) {
279
+ const body = { body: comment };
280
+
281
+ const options = GitLab.baseOptionsHttpReq();
282
+
283
+ options.method = 'POST';
284
+ options.body = JSON.stringify(body);
285
+ options.headers = {
286
+ 'Content-Type': 'application/json',
287
+ ...options.headers,
288
+ };
289
+
290
+ try {
291
+ const response = await nodeFetch(
292
+ `${this.apiBaseURL}/projects/${this.projectId}/issues/${issue.iid}/notes`,
293
+ options,
294
+ );
295
+ const res = await response.json();
296
+
297
+ if (response.ok) {
298
+ logger.info(`Added comment to GitLab issue #${issue.iid} ${issue.title}: ${res.id}`);
299
+
300
+ return res.body;
301
+ }
302
+
303
+ logger.error(`openIssue response: ${JSON.stringify(res)}`);
304
+ } catch (error) {
305
+ logger.error(`Could not add comment to GitLab issue #${issue.iid} "${issue.title}": ${error}`);
306
+ }
307
+ }
308
+
309
+ async closeIssueWithCommentIfExists({ title, comment }) {
310
+ const issue = await this.getIssue({
311
+ title,
312
+ state: GitLab.ISSUE_STATE_OPEN,
313
+ });
314
+
315
+ // if issue does not exist in the "opened" state
316
+ if (!issue) {
317
+ return;
318
+ }
319
+
320
+ await this.addCommentToIssue({ issue, comment });
321
+
322
+ return this.closeIssue(issue);
323
+ }
324
+
325
+ async createOrUpdateIssue({ title, description, label }) {
326
+ const issue = await this.getIssue({ title, state: GitLab.ISSUE_STATE_ALL });
327
+
328
+ if (!issue) {
329
+ return this.createIssue({ title, description, labels: [label] });
330
+ }
331
+
332
+ if (issue.state == GitLab.ISSUE_STATE_CLOSED) {
333
+ await this.openIssue(issue);
334
+ }
335
+
336
+ const managedLabelsNames = this.MANAGED_LABELS.map(label => label.name);
337
+ const [managedLabel] = issue.labels.filter(label =>
338
+ managedLabelsNames.includes(label.name)); // it is assumed that only one specific reason for failure is possible at a time, making managed labels mutually exclusive
339
+
340
+ if (managedLabel?.name == label) {
341
+ // if the label is already assigned to the issue, the error is redundant with the one already reported and no further action is necessary
342
+ return;
343
+ }
344
+
345
+ const labelsNotManagedToKeep = issue.labels
346
+ .map(label => label.name)
347
+ .filter(label => !managedLabelsNames.includes(label));
348
+
349
+ await this.setIssueLabels({
350
+ issue,
351
+ labels: [ label, ...labelsNotManagedToKeep ],
352
+ });
353
+ await this.addCommentToIssue({ issue, comment: description });
354
+ }
355
+
356
+ static baseOptionsHttpReq(token = process.env.OTA_ENGINE_GITLAB_TOKEN) {
357
+ const options = {};
358
+
359
+ if (process.env.HTTPS_PROXY) {
360
+ options.agent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
361
+ } else if (process.env.HTTP_PROXY) {
362
+ options.agent = new HttpProxyAgent(process.env.HTTP_PROXY);
363
+ }
364
+
365
+ options.headers = { Authorization: `Bearer ${token}` };
366
+
367
+ return options;
368
+ }
369
+
370
+ generateDeclarationURL(serviceName) {
371
+ return `${this.baseURL}/${this.commonParams.owner}/${this.commonParams.repo}/-/blob/main/declarations/${encodeURIComponent(serviceName)}.json`;
372
+ }
373
+
374
+ generateVersionURL(serviceName, termsType) {
375
+ return `${this.baseURL}/${this.commonParams.owner}/${this.commonParams.repo}/-/blob/main/${encodeURIComponent(serviceName)}/${encodeURIComponent(serviceName, termsType)}.md`;
376
+ }
377
+
378
+ generateSnapshotsBaseUrl(serviceName, termsType) {
379
+ return `${this.baseURL}/${this.commonParams.owner}/${this.commonParams.repo}/-/blob/main/${encodeURIComponent(serviceName)}/${encodeURIComponent(termsType)}`;
380
+ }
381
+
382
+ // GitLab API responses are not cached unlike GitHub, so this method only exists to satisfy the Reporter interface contract
383
+ clearCache() { /* eslint-disable-line class-methods-use-this */
384
+ logger.debug('Cache clearing not implemented for GitLab reporter as it is not needed');
385
+ }
386
+ }