@opentermsarchive/engine 2.4.0 → 2.6.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/package.json +1 -1
- package/scripts/dataset/publish/github/index.js +36 -0
- package/scripts/dataset/publish/gitlab/index.js +133 -0
- package/scripts/dataset/publish/index.js +11 -32
- package/scripts/reporter/duplicate/README.md +37 -0
- package/scripts/reporter/duplicate/index.js +73 -0
- package/src/index.js +9 -13
- package/src/reporter/factory.js +13 -0
- package/src/reporter/{github.js → github/index.js} +14 -2
- package/src/reporter/{github.test.js → github/index.test.js} +1 -1
- package/src/reporter/{labels.test.js → github/labels.test.js} +1 -1
- package/src/reporter/gitlab/index.js +386 -0
- package/src/reporter/gitlab/index.test.js +527 -0
- package/src/reporter/gitlab/labels.json +77 -0
- package/src/reporter/gitlab/labels.test.js +30 -0
- package/src/reporter/index.js +54 -16
- package/src/reporter/index.test.js +63 -0
- /package/src/reporter/{labels.json → github/labels.json} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fsApi from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import url from 'url';
|
|
4
|
+
|
|
5
|
+
import config from 'config';
|
|
6
|
+
import { Octokit } from 'octokit';
|
|
7
|
+
|
|
8
|
+
import * as readme from '../../assets/README.template.js';
|
|
9
|
+
|
|
10
|
+
export default async function publish({ archivePath, releaseDate, stats }) {
|
|
11
|
+
const octokit = new Octokit({ auth: process.env.OTA_ENGINE_GITHUB_TOKEN });
|
|
12
|
+
|
|
13
|
+
const [ owner, repo ] = url.parse(config.get('@opentermsarchive/engine.dataset.versionsRepositoryURL')).pathname.split('/').filter(component => component);
|
|
14
|
+
|
|
15
|
+
const tagName = `${path.basename(archivePath, path.extname(archivePath))}`; // use archive filename as Git tag
|
|
16
|
+
|
|
17
|
+
const { data: { upload_url: uploadUrl, html_url: releaseUrl } } = await octokit.rest.repos.createRelease({
|
|
18
|
+
owner,
|
|
19
|
+
repo,
|
|
20
|
+
tag_name: tagName,
|
|
21
|
+
name: readme.title({ releaseDate }),
|
|
22
|
+
body: readme.body(stats),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await octokit.rest.repos.uploadReleaseAsset({
|
|
26
|
+
data: fsApi.readFileSync(archivePath),
|
|
27
|
+
headers: {
|
|
28
|
+
'content-type': 'application/zip',
|
|
29
|
+
'content-length': fsApi.statSync(archivePath).size,
|
|
30
|
+
},
|
|
31
|
+
name: path.basename(archivePath),
|
|
32
|
+
url: uploadUrl,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return releaseUrl;
|
|
36
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fsApi from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import config from 'config';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import FormData from 'form-data';
|
|
7
|
+
import nodeFetch from 'node-fetch';
|
|
8
|
+
|
|
9
|
+
import GitLab from '../../../../src/reporter/gitlab/index.js';
|
|
10
|
+
import * as readme from '../../assets/README.template.js';
|
|
11
|
+
import logger from '../../logger/index.js';
|
|
12
|
+
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
export default async function publish({
|
|
16
|
+
archivePath,
|
|
17
|
+
releaseDate,
|
|
18
|
+
stats,
|
|
19
|
+
}) {
|
|
20
|
+
let projectId = null;
|
|
21
|
+
const gitlabAPIUrl = config.get('@opentermsarchive/engine.dataset.apiBaseURL');
|
|
22
|
+
|
|
23
|
+
const [ owner, repo ] = new URL(config.get('@opentermsarchive/engine.dataset.versionsRepositoryURL'))
|
|
24
|
+
.pathname
|
|
25
|
+
.split('/')
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
const commonParams = { owner, repo };
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const repositoryPath = `${commonParams.owner}/${commonParams.repo}`;
|
|
31
|
+
|
|
32
|
+
const options = GitLab.baseOptionsHttpReq(process.env.OTA_ENGINE_GITLAB_RELEASES_TOKEN);
|
|
33
|
+
|
|
34
|
+
options.method = 'GET';
|
|
35
|
+
options.headers = {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
...options.headers,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const response = await nodeFetch(
|
|
41
|
+
`${gitlabAPIUrl}/projects/${encodeURIComponent(repositoryPath)}`,
|
|
42
|
+
options,
|
|
43
|
+
);
|
|
44
|
+
const res = await response.json();
|
|
45
|
+
|
|
46
|
+
projectId = res.id;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error(`Error while obtaining projectId: ${error}`);
|
|
49
|
+
projectId = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tagName = `${path.basename(archivePath, path.extname(archivePath))}`; // use archive filename as Git tag
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
let options = GitLab.baseOptionsHttpReq(process.env.OTA_ENGINE_GITLAB_RELEASES_TOKEN);
|
|
56
|
+
|
|
57
|
+
options.method = 'POST';
|
|
58
|
+
options.body = {
|
|
59
|
+
ref: 'main',
|
|
60
|
+
tag_name: tagName,
|
|
61
|
+
name: readme.title({ releaseDate }),
|
|
62
|
+
description: readme.body(stats),
|
|
63
|
+
};
|
|
64
|
+
options.headers = {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
...options.headers,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
options.body = JSON.stringify(options.body);
|
|
70
|
+
|
|
71
|
+
const releaseResponse = await nodeFetch(
|
|
72
|
+
`${gitlabAPIUrl}/projects/${projectId}/releases`,
|
|
73
|
+
options,
|
|
74
|
+
);
|
|
75
|
+
const releaseRes = await releaseResponse.json();
|
|
76
|
+
|
|
77
|
+
const releaseId = releaseRes.commit.id;
|
|
78
|
+
|
|
79
|
+
logger.info(`Created release with releaseId: ${releaseId}`);
|
|
80
|
+
|
|
81
|
+
// Upload the package
|
|
82
|
+
options = GitLab.baseOptionsHttpReq(process.env.OTA_ENGINE_GITLAB_RELEASES_TOKEN);
|
|
83
|
+
options.method = 'PUT';
|
|
84
|
+
options.body = fsApi.createReadStream(archivePath);
|
|
85
|
+
|
|
86
|
+
// restrict characters to the ones allowed by GitLab APIs
|
|
87
|
+
const packageName = config.get('@opentermsarchive/engine.dataset.title').replace(/[^a-zA-Z0-9.\-_]/g, '-');
|
|
88
|
+
const packageVersion = tagName.replace(/[^a-zA-Z0-9.\-_]/g, '-');
|
|
89
|
+
const packageFileName = archivePath.replace(/[^a-zA-Z0-9.\-_/]/g, '-');
|
|
90
|
+
|
|
91
|
+
logger.debug(`packageName: ${packageName}, packageVersion: ${packageVersion} packageFileName: ${packageFileName}`);
|
|
92
|
+
|
|
93
|
+
const packageResponse = await nodeFetch(
|
|
94
|
+
`${gitlabAPIUrl}/projects/${projectId}/packages/generic/${packageName}/${packageVersion}/${packageFileName}?status=default&select=package_file`,
|
|
95
|
+
options,
|
|
96
|
+
);
|
|
97
|
+
const packageRes = await packageResponse.json();
|
|
98
|
+
|
|
99
|
+
const packageFilesId = packageRes.id;
|
|
100
|
+
|
|
101
|
+
logger.debug(`package file id: ${packageFilesId}`);
|
|
102
|
+
|
|
103
|
+
// use the package id to build the download url for the release
|
|
104
|
+
const publishedPackageUrl = `${config.get('@opentermsarchive/engine.dataset.versionsRepositoryURL')}/-/package_files/${packageFilesId}/download`;
|
|
105
|
+
|
|
106
|
+
// Create the release and link the package
|
|
107
|
+
const formData = new FormData();
|
|
108
|
+
|
|
109
|
+
formData.append('name', archivePath);
|
|
110
|
+
formData.append('url', publishedPackageUrl);
|
|
111
|
+
formData.append('file', fsApi.createReadStream(archivePath), { filename: path.basename(archivePath) });
|
|
112
|
+
|
|
113
|
+
options = GitLab.baseOptionsHttpReq(process.env.OTA_ENGINE_GITLAB_RELEASES_TOKEN);
|
|
114
|
+
options.method = 'POST';
|
|
115
|
+
options.headers = {
|
|
116
|
+
...formData.getHeaders(),
|
|
117
|
+
...options.headers,
|
|
118
|
+
};
|
|
119
|
+
options.body = formData;
|
|
120
|
+
|
|
121
|
+
const uploadResponse = await nodeFetch(
|
|
122
|
+
`${gitlabAPIUrl}/projects/${projectId}/releases/${tagName}/assets/links`,
|
|
123
|
+
options,
|
|
124
|
+
);
|
|
125
|
+
const uploadRes = await uploadResponse.json();
|
|
126
|
+
const releaseUrl = uploadRes.direct_asset_url;
|
|
127
|
+
|
|
128
|
+
return releaseUrl;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.error('Failed to create release or upload ZIP file:', error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import url from 'url';
|
|
1
|
+
import publishGitHub from './github/index.js';
|
|
2
|
+
import publishGitLab from './gitlab/index.js';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export default function publishRelease({ archivePath, releaseDate, stats }) {
|
|
5
|
+
// If both GitHub and GitLab tokens are defined, GitHub takes precedence
|
|
6
|
+
if (process.env.OTA_ENGINE_GITHUB_TOKEN) {
|
|
7
|
+
return publishGitHub({ archivePath, releaseDate, stats });
|
|
8
|
+
}
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
if (process.env.OTA_ENGINE_GITLAB_TOKEN) {
|
|
11
|
+
return publishGitLab({ archivePath, releaseDate, stats });
|
|
12
|
+
}
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
const octokit = new Octokit({ auth: process.env.OTA_ENGINE_GITHUB_TOKEN });
|
|
12
|
-
|
|
13
|
-
const [ owner, repo ] = url.parse(config.get('@opentermsarchive/engine.dataset.versionsRepositoryURL')).pathname.split('/').filter(component => component);
|
|
14
|
-
|
|
15
|
-
const tagName = `${path.basename(archivePath, path.extname(archivePath))}`; // use archive filename as Git tag
|
|
16
|
-
|
|
17
|
-
const { data: { upload_url: uploadUrl, html_url: releaseUrl } } = await octokit.rest.repos.createRelease({
|
|
18
|
-
owner,
|
|
19
|
-
repo,
|
|
20
|
-
tag_name: tagName,
|
|
21
|
-
name: readme.title({ releaseDate }),
|
|
22
|
-
body: readme.body(stats),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
await octokit.rest.repos.uploadReleaseAsset({
|
|
26
|
-
data: fsApi.readFileSync(archivePath),
|
|
27
|
-
headers: {
|
|
28
|
-
'content-type': 'application/zip',
|
|
29
|
-
'content-length': fsApi.statSync(archivePath).size,
|
|
30
|
-
},
|
|
31
|
-
name: path.basename(archivePath),
|
|
32
|
-
url: uploadUrl,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
return releaseUrl;
|
|
14
|
+
throw new Error('No GitHub nor GitLab token found in environment variables (OTA_ENGINE_GITHUB_TOKEN or OTA_ENGINE_GITLAB_TOKEN). Cannot publish the dataset without authentication.');
|
|
36
15
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Duplicate issues removal script
|
|
2
|
+
|
|
3
|
+
This script helps remove duplicate issues from a GitHub repository by closing issues that have the same title as any older issue.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. Set up environment variables:
|
|
8
|
+
- Create a `.env` file in the root directory
|
|
9
|
+
- Add the GitHub personal access token of the bot that manages issues on your collection, with `repo` permissions:
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
OTA_ENGINE_GITHUB_TOKEN=your_github_token
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. Configure the target repository in your chosen configuration file within the `config` folder:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"@opentermsarchive/engine": {
|
|
20
|
+
"reporter": {
|
|
21
|
+
"githubIssues": {
|
|
22
|
+
"repositories": {
|
|
23
|
+
"declarations": "owner/repository"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Run the script using:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
node scripts/reporter/duplicate/index.js
|
|
37
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import config from 'config';
|
|
3
|
+
import { Octokit } from 'octokit';
|
|
4
|
+
|
|
5
|
+
async function removeDuplicateIssues() {
|
|
6
|
+
const repository = config.get('@opentermsarchive/engine.reporter.githubIssues.repositories.declarations');
|
|
7
|
+
|
|
8
|
+
if (!repository.includes('/') || repository.includes('https://')) {
|
|
9
|
+
throw new Error(`Configuration entry "reporter.githubIssues.repositories.declarations" is expected to be a string in the format <owner>/<repo>, but received: "${repository}"`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [ owner, repo ] = repository.split('/');
|
|
13
|
+
|
|
14
|
+
const octokit = new Octokit({ auth: process.env.OTA_ENGINE_GITHUB_TOKEN });
|
|
15
|
+
|
|
16
|
+
console.log(`Getting issues from repository ${repository}…`);
|
|
17
|
+
|
|
18
|
+
const issues = await octokit.paginate('GET /repos/{owner}/{repo}/issues', {
|
|
19
|
+
owner,
|
|
20
|
+
repo,
|
|
21
|
+
state: 'open',
|
|
22
|
+
per_page: 100,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const onlyIssues = issues.filter(issue => !issue.pull_request);
|
|
26
|
+
const issuesByTitle = new Map();
|
|
27
|
+
let counter = 0;
|
|
28
|
+
|
|
29
|
+
console.log(`Found ${onlyIssues.length} issues`);
|
|
30
|
+
|
|
31
|
+
for (const issue of onlyIssues) {
|
|
32
|
+
if (!issuesByTitle.has(issue.title)) {
|
|
33
|
+
issuesByTitle.set(issue.title, [issue]);
|
|
34
|
+
} else {
|
|
35
|
+
issuesByTitle.get(issue.title).push(issue);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const [ title, duplicateIssues ] of issuesByTitle) {
|
|
40
|
+
if (duplicateIssues.length === 1) continue;
|
|
41
|
+
|
|
42
|
+
const originalIssue = duplicateIssues.reduce((oldest, current) => (new Date(current.created_at) < new Date(oldest.created_at) ? current : oldest));
|
|
43
|
+
|
|
44
|
+
console.log(`\nFound ${duplicateIssues.length - 1} duplicates for issue #${originalIssue.number} "${title}"`);
|
|
45
|
+
|
|
46
|
+
for (const issue of duplicateIssues) {
|
|
47
|
+
if (issue.number === originalIssue.number) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', { /* eslint-disable-line no-await-in-loop */
|
|
52
|
+
owner,
|
|
53
|
+
repo,
|
|
54
|
+
issue_number: issue.number,
|
|
55
|
+
state: 'closed',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', { /* eslint-disable-line no-await-in-loop */
|
|
59
|
+
owner,
|
|
60
|
+
repo,
|
|
61
|
+
issue_number: issue.number,
|
|
62
|
+
body: `This issue is detected as duplicate as it has the same title as #${originalIssue.number}. It most likely was created accidentally by an engine older than [v2.3.2](https://github.com/OpenTermsArchive/engine/releases/tag/v2.3.2). Closing automatically.`,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
counter++;
|
|
66
|
+
console.log(`Closed issue #${issue.number}: ${issue.html_url}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`\nDuplicate removal process completed; ${counter} issues closed`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
removeDuplicateIssues();
|
package/src/index.js
CHANGED
|
@@ -55,21 +55,17 @@ export default async function track({ services, types, extractOnly, schedule })
|
|
|
55
55
|
logger.warn('Environment variable "OTA_ENGINE_SENDINBLUE_API_KEY" was not found; the Notifier module will be ignored');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
if (process.env.OTA_ENGINE_GITHUB_TOKEN) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logger.error('Cannot instantiate the Reporter module; it will be ignored:', error);
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
logger.warn('Configuration key "reporter.githubIssues.repositories.declarations" was not found; issues on the declarations repository cannot be created');
|
|
58
|
+
if (process.env.OTA_ENGINE_GITHUB_TOKEN || process.env.OTA_ENGINE_GITLAB_TOKEN) {
|
|
59
|
+
try {
|
|
60
|
+
const reporter = new Reporter(config.get('@opentermsarchive/engine.reporter'));
|
|
61
|
+
|
|
62
|
+
await reporter.initialize();
|
|
63
|
+
archivist.attach(reporter);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error('Cannot instantiate the Reporter module; it will be ignored:', error);
|
|
70
66
|
}
|
|
71
67
|
} else {
|
|
72
|
-
logger.warn('Environment variable
|
|
68
|
+
logger.warn('Environment variable with token for GitHub or GitLab was not found; the Reporter module will be ignored');
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
if (!schedule) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import GitHub from './github/index.js';
|
|
2
|
+
import GitLab from './gitlab/index.js';
|
|
3
|
+
|
|
4
|
+
export function createReporter(config) {
|
|
5
|
+
switch (config.type) {
|
|
6
|
+
case 'github':
|
|
7
|
+
return new GitHub(config.repositories.declarations);
|
|
8
|
+
case 'gitlab':
|
|
9
|
+
return new GitLab(config.repositories.declarations, config.baseURL, config.apiBaseURL);
|
|
10
|
+
default:
|
|
11
|
+
throw new Error(`Unsupported reporter type: ${config.type}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -2,7 +2,7 @@ import { createRequire } from 'module';
|
|
|
2
2
|
|
|
3
3
|
import { Octokit } from 'octokit';
|
|
4
4
|
|
|
5
|
-
import logger from '
|
|
5
|
+
import logger from '../../logger/index.js';
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ export default class GitHub {
|
|
|
14
14
|
static ISSUE_STATE_ALL = 'all';
|
|
15
15
|
|
|
16
16
|
constructor(repository) {
|
|
17
|
-
const { version } = require('
|
|
17
|
+
const { version } = require('../../../package.json');
|
|
18
18
|
|
|
19
19
|
this.octokit = new Octokit({
|
|
20
20
|
auth: process.env.OTA_ENGINE_GITHUB_TOKEN,
|
|
@@ -198,4 +198,16 @@ export default class GitHub {
|
|
|
198
198
|
logger.error(`Failed to update issue "${title}": ${error.stack}`);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
generateDeclarationURL(serviceName) {
|
|
203
|
+
return `https://github.com/${this.commonParams.owner}/${this.commonParams.repo}/blob/main/declarations/${encodeURIComponent(serviceName)}.json`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
generateVersionURL(serviceName, termsType) {
|
|
207
|
+
return `https://github.com/${this.commonParams.owner}/${this.commonParams.repo}/blob/main/${encodeURIComponent(serviceName)}/${encodeURIComponent(termsType)}.md`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
generateSnapshotsBaseUrl(serviceName, termsType) {
|
|
211
|
+
return `https://github.com/${this.commonParams.owner}/${this.commonParams.repo}/blob/main/${encodeURIComponent(serviceName)}/${encodeURIComponent(termsType)}`;
|
|
212
|
+
}
|
|
201
213
|
}
|