forked from ubiquity/devpool-directory
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
321 lines (294 loc) · 10.5 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/**
* Syncs issues with partner projects
*/
import dotenv from 'dotenv';
import { Octokit } from 'octokit';
import _projects from './projects.json';
interface Projects{
urls: string[];
category?: Record<string, string>
}
const projects = _projects as Projects;
// init env variables
dotenv.config();
const DEVPOOL_OWNER_NAME = "korrrba";
const DEVPOOL_REPO_NAME = "devpool-directory";
type Issue = {
html_url: string,
labels: {
name: string,
}[],
node_id: string,
number: number,
pull_request: null | {},
state: 'open' | 'closed',
title: string,
body?: string;
assignee: {
login: string;
};
}
enum LABELS {
PRICE = 'Price',
UNAVAILABLE = 'Unavailable',
};
// init octokit
const octokit = new Octokit({ auth: process.env.DEVPOOL_GITHUB_API_TOKEN });
/**
* Main function
* TODO: retry on rate limit error
* TODO: handle project deletion
*/
async function main() {
try {
// get devpool issues
const devpoolIssues: Issue[] = await getAllIssues(
DEVPOOL_OWNER_NAME,
DEVPOOL_REPO_NAME
);
// aggregate all project issues
const allProjectIssues: Issue[] = [];
/*
// for each project URL
for (let projectUrl of projects.urls) {
// get owner and repository names from project URL
const [ownerName, repoName] = getRepoCredentials(projectUrl);
// get all project issues (opened and closed)
const projectIssues: Issue[] = await getAllIssues(ownerName, repoName);
// aggregate all project issues
allProjectIssues.push(...projectIssues);
// for all issues
for (let projectIssue of projectIssues) {
// if issue exists in devpool
const devpoolIssue = getIssueByLabel(devpoolIssues, `id: ${projectIssue.node_id}`);
if (devpoolIssue) {
// If project issue doesn't have the "Price" label (i.e. it has been removed) then close
// the devpool issue if it is not already closed, no need to pollute devpool repo with draft issues
if (!projectIssue.labels.some(label => label.name.includes(LABELS.PRICE))) {
if (devpoolIssue.state === 'open') {
await octokit.rest.issues.update({
owner: DEVPOOL_OWNER_NAME,
repo: DEVPOOL_REPO_NAME,
issue_number: devpoolIssue.number,
state: 'closed',
});
console.log(`Closed (price label not set): ${devpoolIssue.html_url} (${projectIssue.html_url})`);
} else {
console.log(`Already closed (price label not set): ${devpoolIssue.html_url} (${projectIssue.html_url})`);
}
continue;
}
// prepare for issue updating
const isDevpoolUnavailableLabel = devpoolIssue.labels?.some((label) => label.name === LABELS.UNAVAILABLE);
const devpoolIssueLabelsStringified = devpoolIssue.labels.map(label => label.name).sort().toString();
const projectIssueLabelsStringified = getDevpoolIssueLabels(projectIssue, projectUrl).sort().toString();
// Update devpool issue if any of the following has been updated:
// - issue title
// - issue state (open/closed)
// - assignee (exists or not)
// - repository name (devpool issue body contains a partner project issue URL)
// - any label
if (devpoolIssue.title !== projectIssue.title ||
devpoolIssue.state !== projectIssue.state ||
(!isDevpoolUnavailableLabel && projectIssue.assignee?.login) ||
(isDevpoolUnavailableLabel && !projectIssue.assignee?.login) ||
devpoolIssue.body !== projectIssue.html_url ||
devpoolIssueLabelsStringified !== projectIssueLabelsStringified
) {
await octokit.rest.issues.update({
owner: DEVPOOL_OWNER_NAME,
repo: DEVPOOL_REPO_NAME,
issue_number: devpoolIssue.number,
title: projectIssue.title,
body: projectIssue.html_url,
state: projectIssue.state,
labels: getDevpoolIssueLabels(projectIssue, projectUrl),
});
console.log(`Updated: ${devpoolIssue.html_url} (${projectIssue.html_url})`);
} else {
console.log(`No updates: ${devpoolIssue.html_url} (${projectIssue.html_url})`);
}
} else {
// issue does not exist in devpool
// if issue is "closed" then skip it, no need to copy/paste already "closed" issues
if (projectIssue.state === 'closed') continue;
// if issue doesn't have the "Price" label then skip it, no need to pollute repo with draft issues
if (!projectIssue.labels.some(label => label.name.includes(LABELS.PRICE))) continue;
// create a new issue
const createdIssue = await octokit.rest.issues.create({
owner: DEVPOOL_OWNER_NAME,
repo: DEVPOOL_REPO_NAME,
title: projectIssue.title,
body: projectIssue.html_url,
labels: getDevpoolIssueLabels(projectIssue, projectUrl),
});
console.log(`Created: ${createdIssue.data.html_url} (${projectIssue.html_url})`);
}
}
}
// close missing issues
await forceCloseMissingIssues(devpoolIssues, allProjectIssues);
*/
} catch (err) {
console.log(err);
}
}
main();
//=============
// Helpers
//=============
/**
* Deletes github issue
* @param nodeId issue node id
*/
async function deleteIssue(nodeId: string) {
await octokit.graphql(
`
mutation($input:DeleteIssueInput!) {
deleteIssue(input:$input) {
clientMutationId
}
}
`,
{
input: {
issueId: nodeId,
clientMutationId: 'devpool',
}
}
);
}
/**
* Closes issues that exist in the devpool but are missing in partner projects
*
* Devpool and partner project issues can be
* out of sync in the following cases:
* - partner project issue was deleted or transferred to another repo
* - partner project repo was deleted from https://github.com/ubiquity/devpool-directory/blob/development/projects.json
* - partner project repo was made private
* @param devpoolIssues all devpool issues array
* @param projectIssues all partner project issues array
*/
async function forceCloseMissingIssues(
devpoolIssues: Issue[],
projectIssues: Issue[],
) {
// for all devpool issues
for (let devpoolIssue of devpoolIssues) {
// if devpool issue does not exist in partners' projects then close it
if (!projectIssues.some(projectIssue => projectIssue.node_id === getIssueLabelValue(devpoolIssue, 'id:'))) {
if (devpoolIssue.state === 'open') {
await octokit.rest.issues.update({
owner: DEVPOOL_OWNER_NAME,
repo: DEVPOOL_REPO_NAME,
issue_number: devpoolIssue.number,
state: 'closed',
});
console.log(`Closed (missing in partners projects): ${devpoolIssue.html_url}`);
} else {
console.log(`Already closed (missing in partners projects): ${devpoolIssue.html_url}`);
}
}
}
}
/**
* Returns all issues in a repo
* @param ownerName owner name
* @param repoName repo name
* @returns array of issues
*/
async function getAllIssues(ownerName: string, repoName: string) {
// get all project issues (opened and closed)
let issues: Issue[] = await octokit.paginate({
method: "GET",
url: `/repos/${ownerName}/${repoName}/issues?state=all`,
});
// remove PRs from the project issues
issues = issues.filter((issue) => !issue.pull_request);
return issues;
}
/**
* Returns array of labels for a devpool issue
* @param issue issue object
*/
function getDevpoolIssueLabels(
issue: Issue,
projectUrl: string
) {
// get owner and repo name from issue's URL because the repo name could be updated
const [ownerName, repoName] = getRepoCredentials(issue.html_url);
// default labels
const devpoolIssueLabels = [
getIssuePriceLabel(issue), // price
`Partner: ${ownerName}/${repoName}`, // partner
`id: ${issue.node_id}`, // id
];
// if project is already assigned then add the "Unavailable" label
if (issue.assignee?.login) devpoolIssueLabels.push(LABELS.UNAVAILABLE);
// add all missing labels that exist in a project's issue and don't exist in devpool issue
for (let projectIssueLabel of issue.labels) {
// skip the "Price" label in order to not accidentally generate a permit
if (projectIssueLabel.name.includes('Price')) continue;
// if project issue label does not exist in devpool issue then add it
if (!devpoolIssueLabels.includes(projectIssueLabel.name)) devpoolIssueLabels.push(projectIssueLabel.name);
}
// if project category for the project is defined, add its category label
if (projects.category && projectUrl in projects.category) devpoolIssueLabels.push(projects.category[projectUrl]);
return devpoolIssueLabels;
}
/**
* Returns issue by label
* @param issues issues array
* @param label label string
*/
function getIssueByLabel(issues: Issue[], label: string) {
issues = issues.filter((issue) => {
const labels = issue.labels.filter((obj) => obj.name === label);
return labels.length > 0;
});
return issues.length > 0 ? issues[0] : null;
}
/**
* Returns label value by label prefix
* Example: "Partner: my/repo" => "my/repo"
* Example: "id: 123qwe" => "123qwe"
* @param issue issue
* @param labelPrefix label prefix
*/
function getIssueLabelValue(issue: Issue, labelPrefix: string) {
let labelValue = null;
for (let labelObj of issue.labels) {
if (labelObj.name.includes(labelPrefix)) {
labelValue = labelObj.name.split(':')[1].trim();
break;
}
}
return labelValue;
}
/**
* Returns price label from an issue
* @param issue issue object
* @returns price label
*/
function getIssuePriceLabel(issue: Issue) {
let defaultPriceLabel = "Pricing: not set";
let priceLabels = issue.labels.filter(
(label) => label.name.includes("Price:") || label.name.includes("Pricing:")
);
// NOTICE: we rename "Price" to "Pricing" because the bot removes all manually added price labels starting with "Price:"
return priceLabels.length > 0
? priceLabels[0].name.replace("Price", "Pricing")
: defaultPriceLabel;
}
/**
* Returns owner and repository names from a project URL
* @param projectUrl project URL
* @returns array of owner and repository names
*/
function getRepoCredentials(projectUrl: string) {
const urlObject = new URL(projectUrl);
const urlPath = urlObject.pathname.split('/');
const ownerName = urlPath[1];
const repoName = urlPath[2];
return [ownerName, repoName];
}