wuffle 0.73.2 → 0.74.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/LICENSE +21 -0
- package/lib/apps/{automatic-dev-flow.js → automatic-dev-flow/AutomaticDevFlow.js} +6 -28
- package/lib/apps/automatic-dev-flow/index.js +6 -0
- package/lib/apps/background-sync/BackgroundSync.js +32 -102
- package/lib/apps/background-sync/BackgroundSyncBackend.js +140 -0
- package/lib/apps/background-sync/index.js +3 -1
- package/lib/apps/{events-sync.js → events-sync/EventsSync.js} +7 -5
- package/lib/apps/events-sync/index.js +6 -0
- package/lib/apps/github-app/GithubApp.js +69 -6
- package/lib/apps/github-comments/GithubComments.js +11 -3
- package/lib/apps/github-reviews/GithubReviews.js +11 -3
- package/lib/apps/github-statuses/GithubStatuses.js +11 -3
- package/lib/apps/issue-filter/IssueFilter.js +42 -7
- package/lib/apps/log-events.js +4 -4
- package/lib/apps/{reindex-store.js → reindex-store/ReindexStore.js} +6 -4
- package/lib/apps/reindex-store/index.js +6 -0
- package/lib/index.js +13 -5
- package/package.json +6 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-present Nico Rehwaldt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { filterIssueOrPull } from '../filters.js';
|
|
2
|
-
import { issueIdent } from '../util/meta.js';
|
|
3
|
-
|
|
4
1
|
const DONE = 'DONE';
|
|
5
2
|
const EXTERNAL_CONTRIBUTION = 'EXTERNAL_CONTRIBUTION';
|
|
6
3
|
const IN_PROGRESS = 'IN_PROGRESS';
|
|
@@ -15,11 +12,11 @@ const CHANGES_REQUESTED = 'changes_requested';
|
|
|
15
12
|
*
|
|
16
13
|
* @constructor
|
|
17
14
|
*
|
|
18
|
-
* @param {import('
|
|
19
|
-
* @param {import('
|
|
20
|
-
* @param {import('
|
|
21
|
-
* @param {import('
|
|
22
|
-
* @param {import('
|
|
15
|
+
* @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
|
|
16
|
+
* @param {import('../github-issues/GithubIssues.js').default} githubIssues
|
|
17
|
+
* @param {import('../../columns.js').default} columns
|
|
18
|
+
* @param {import('../issue-filter/IssueFilter.js').default} issueFilter
|
|
19
|
+
* @param {import('../../types.js').Logger } logger
|
|
23
20
|
*/
|
|
24
21
|
export default function(webhookEvents, githubIssues, columns, issueFilter, logger) {
|
|
25
22
|
|
|
@@ -27,26 +24,7 @@ export default function(webhookEvents, githubIssues, columns, issueFilter, logge
|
|
|
27
24
|
name: 'wuffle:automatic-dev-flow'
|
|
28
25
|
});
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return (context) => {
|
|
33
|
-
|
|
34
|
-
const payload = context.payload;
|
|
35
|
-
|
|
36
|
-
const issueOrPull = filterIssueOrPull(
|
|
37
|
-
payload.issue || payload.pull_request,
|
|
38
|
-
payload.repository
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
if (issueFilter.isIgnored(issueOrPull)) {
|
|
42
|
-
log.debug({ issue: issueIdent(issueOrPull) }, 'issue matching ignore filter');
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return webhookHandlerFn(context);
|
|
48
|
-
};
|
|
49
|
-
}
|
|
27
|
+
const ifEnabled = issueFilter.createWebhookFilter(log);
|
|
50
28
|
|
|
51
29
|
webhookEvents.on([
|
|
52
30
|
'issues.closed',
|
|
@@ -11,14 +11,13 @@ function isInternalError(error) {
|
|
|
11
11
|
*
|
|
12
12
|
* @constructor
|
|
13
13
|
*
|
|
14
|
-
* @param {import('../../types.js').Logger} logger
|
|
15
14
|
* @param {Object} config
|
|
15
|
+
* @param {import('../../types.js').Logger} logger
|
|
16
16
|
* @param {import('../../store.js').default} store
|
|
17
|
-
* @param {import('../github-client/GithubClient.js').default} githubClient
|
|
18
|
-
* @param {import('../github-app/GithubApp.js').default} githubApp
|
|
19
17
|
* @param {import('../../events.js').default} events
|
|
18
|
+
* @param {import('./BackgroundSyncBackend.js').default} backgroundSyncBackend
|
|
20
19
|
*/
|
|
21
|
-
export default function BackgroundSync(
|
|
20
|
+
export default function BackgroundSync(config, logger, store, events, backgroundSyncBackend) {
|
|
22
21
|
|
|
23
22
|
// 30 days
|
|
24
23
|
const syncClosedLookback = (
|
|
@@ -101,18 +100,14 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
101
100
|
log.debug({ installation: owner }, 'processing');
|
|
102
101
|
|
|
103
102
|
try {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const repositories = await octokit.paginate(
|
|
107
|
-
octokit.rest.apps.listReposAccessibleToInstallation,
|
|
108
|
-
{
|
|
109
|
-
per_page: 100
|
|
110
|
-
}
|
|
111
|
-
);
|
|
103
|
+
const repositories = await backgroundSyncBackend.getInstallationRepositories(installation);
|
|
112
104
|
|
|
113
105
|
for (const repository of repositories) {
|
|
114
106
|
|
|
115
|
-
|
|
107
|
+
if (repository.owner.login !== owner) {
|
|
108
|
+
throw new Error('repository.owner !== installation.owner');
|
|
109
|
+
}
|
|
110
|
+
|
|
116
111
|
const repo = repository.name;
|
|
117
112
|
|
|
118
113
|
// log found repository
|
|
@@ -135,74 +130,12 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
135
130
|
repo
|
|
136
131
|
}, 'processing');
|
|
137
132
|
|
|
138
|
-
const
|
|
139
|
-
sort: /** @type { 'updated' } */ ('updated'),
|
|
140
|
-
direction: /** @type { 'desc' } */ ('desc'),
|
|
141
|
-
per_page: 100,
|
|
142
|
-
owner,
|
|
143
|
-
repo
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const [
|
|
133
|
+
const {
|
|
147
134
|
open_issues,
|
|
148
135
|
closed_issues,
|
|
149
136
|
open_pull_requests,
|
|
150
137
|
closed_pull_requests
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// open issues
|
|
154
|
-
octokit.paginate(
|
|
155
|
-
octokit.rest.issues.listForRepo,
|
|
156
|
-
{
|
|
157
|
-
...params,
|
|
158
|
-
state: 'open'
|
|
159
|
-
},
|
|
160
|
-
response => response.data.filter(issue => !('pull_request' in issue))
|
|
161
|
-
),
|
|
162
|
-
|
|
163
|
-
// closed issues, updated last 30 days
|
|
164
|
-
octokit.paginate(
|
|
165
|
-
octokit.rest.issues.listForRepo,
|
|
166
|
-
{
|
|
167
|
-
...params,
|
|
168
|
-
state: 'closed',
|
|
169
|
-
since: new Date(syncClosedSince).toISOString()
|
|
170
|
-
},
|
|
171
|
-
response => response.data.filter(issue => !('pull_request' in issue))
|
|
172
|
-
),
|
|
173
|
-
|
|
174
|
-
// open pulls, all
|
|
175
|
-
octokit.paginate(
|
|
176
|
-
octokit.rest.pulls.list,
|
|
177
|
-
{
|
|
178
|
-
...params,
|
|
179
|
-
state: 'open'
|
|
180
|
-
}
|
|
181
|
-
),
|
|
182
|
-
|
|
183
|
-
// closed pulls, updated last 30 days
|
|
184
|
-
octokit.paginate(
|
|
185
|
-
octokit.rest.pulls.list,
|
|
186
|
-
{
|
|
187
|
-
...params,
|
|
188
|
-
state: 'closed'
|
|
189
|
-
},
|
|
190
|
-
(response, done) => {
|
|
191
|
-
|
|
192
|
-
const pulls = response.data;
|
|
193
|
-
|
|
194
|
-
const filtered = pulls.filter(
|
|
195
|
-
pull => new Date(pull.updated_at).getTime() > syncClosedSince
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (filtered.length !== pulls.length) {
|
|
199
|
-
done();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return filtered;
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
]);
|
|
138
|
+
} = await backgroundSyncBackend.getRepositoryIssuesAndPulls(repository, syncClosedSince);
|
|
206
139
|
|
|
207
140
|
for (const issueOrPull of [
|
|
208
141
|
...open_issues,
|
|
@@ -217,25 +150,11 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
217
150
|
|
|
218
151
|
const update = filterIssueOrPull(issueOrPull, repository);
|
|
219
152
|
|
|
220
|
-
|
|
221
|
-
id
|
|
222
|
-
} = update;
|
|
153
|
+
foundIssues[update.id] = update;
|
|
223
154
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
foundIssues[id] = null;
|
|
228
|
-
|
|
229
|
-
log.debug({
|
|
230
|
-
[type]: `${owner}/${repo}#${issueOrPull.number}`
|
|
231
|
-
}, 'skipping, as up-to-date');
|
|
232
|
-
} else {
|
|
233
|
-
foundIssues[id] = update;
|
|
234
|
-
|
|
235
|
-
log.debug({
|
|
236
|
-
[type]: `${owner}/${repo}#${issueOrPull.number}`
|
|
237
|
-
}, 'scheduled for update');
|
|
238
|
-
}
|
|
155
|
+
log.debug({
|
|
156
|
+
[type]: `${owner}/${repo}#${issueOrPull.number}`
|
|
157
|
+
}, 'scheduled for update');
|
|
239
158
|
} catch (err) {
|
|
240
159
|
log.error({
|
|
241
160
|
err,
|
|
@@ -395,12 +314,21 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
395
314
|
|
|
396
315
|
// update changed issues
|
|
397
316
|
|
|
398
|
-
|
|
317
|
+
const updateTasks = pendingUpdates.map(
|
|
318
|
+
update => store.updateIssue(update)
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// returns the update for updated issues or undefined
|
|
322
|
+
// where issues were not stored (filtered out) - we only filter
|
|
323
|
+
// for actual updated issues
|
|
324
|
+
const updatedIssues = await Promise.all(updateTasks).then(updates => {
|
|
325
|
+
return updates.filter(update => update);
|
|
326
|
+
});
|
|
399
327
|
|
|
400
|
-
// emit background sync event for all
|
|
328
|
+
// emit background sync event for all updated issues
|
|
401
329
|
|
|
402
330
|
await syncDetails(
|
|
403
|
-
|
|
331
|
+
updatedIssues,
|
|
404
332
|
getSyncClosedDetailsSince(),
|
|
405
333
|
getSyncOpenDetailsSince()
|
|
406
334
|
);
|
|
@@ -437,10 +365,12 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
437
365
|
);
|
|
438
366
|
}
|
|
439
367
|
|
|
440
|
-
function syncDetails(
|
|
368
|
+
function syncDetails(updatedIssues, syncClosedSince, syncOpenSince) {
|
|
369
|
+
|
|
370
|
+
const jobs = updatedIssues.map(async updatedIssue => {
|
|
441
371
|
|
|
442
|
-
|
|
443
|
-
const issue = await store.getIssueById(id);
|
|
372
|
+
// ensure we fetch latest version of issue (to prevent de-sync)
|
|
373
|
+
const issue = await store.getIssueById(updatedIssue.id);
|
|
444
374
|
|
|
445
375
|
if (!issue) {
|
|
446
376
|
return;
|
|
@@ -468,7 +398,7 @@ We automatically synchronize all repositories you granted us access to via the G
|
|
|
468
398
|
log.info('start');
|
|
469
399
|
|
|
470
400
|
try {
|
|
471
|
-
const installations = await
|
|
401
|
+
const installations = await backgroundSyncBackend.getInstallations();
|
|
472
402
|
|
|
473
403
|
await doSync(installations);
|
|
474
404
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef { import('../github-app/types.js').Installation } Installation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This component fetches GitHub data required for background synchronization.
|
|
7
|
+
*
|
|
8
|
+
* @constructor
|
|
9
|
+
*
|
|
10
|
+
* @param {import('../github-client/GithubClient.js').default} githubClient
|
|
11
|
+
* @param {import('../github-app/GithubApp.js').default} githubApp
|
|
12
|
+
*/
|
|
13
|
+
export default function BackgroundSyncBackend(githubClient, githubApp) {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Return available installations.
|
|
17
|
+
*
|
|
18
|
+
* @return {Promise<Installation[]>}
|
|
19
|
+
*/
|
|
20
|
+
async function getInstallations(installation) {
|
|
21
|
+
return githubApp.getInstallations();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return repositories accessible to a GitHub app installation.
|
|
26
|
+
*
|
|
27
|
+
* @param {Installation} installation
|
|
28
|
+
*
|
|
29
|
+
* @return {Promise<Array>}
|
|
30
|
+
*/
|
|
31
|
+
async function getInstallationRepositories(installation) {
|
|
32
|
+
const owner = installation.account.login;
|
|
33
|
+
const octokit = await githubClient.getOrgScoped(owner);
|
|
34
|
+
|
|
35
|
+
return octokit.paginate(
|
|
36
|
+
octokit.rest.apps.listReposAccessibleToInstallation,
|
|
37
|
+
{ per_page: 100 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Fetch issues and pull requests for a repository.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ owner: { login: string }, name: string }} repository
|
|
45
|
+
* @param {number} syncClosedSince
|
|
46
|
+
*
|
|
47
|
+
* @return {Promise<{ open_issues: Array, closed_issues: Array, open_pull_requests: Array, closed_pull_requests: Array }>}
|
|
48
|
+
*/
|
|
49
|
+
async function getRepositoryIssuesAndPulls(repository, syncClosedSince) {
|
|
50
|
+
const owner = repository.owner.login;
|
|
51
|
+
const octokit = await githubClient.getOrgScoped(owner);
|
|
52
|
+
|
|
53
|
+
const repo = repository.name;
|
|
54
|
+
|
|
55
|
+
const params = {
|
|
56
|
+
sort: /** @type { 'updated' } */ ('updated'),
|
|
57
|
+
direction: /** @type { 'desc' } */ ('desc'),
|
|
58
|
+
per_page: 100,
|
|
59
|
+
owner,
|
|
60
|
+
repo
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const [
|
|
64
|
+
open_issues,
|
|
65
|
+
closed_issues,
|
|
66
|
+
open_pull_requests,
|
|
67
|
+
closed_pull_requests
|
|
68
|
+
] = await Promise.all([
|
|
69
|
+
|
|
70
|
+
// open issues
|
|
71
|
+
octokit.paginate(
|
|
72
|
+
octokit.rest.issues.listForRepo,
|
|
73
|
+
{
|
|
74
|
+
...params,
|
|
75
|
+
state: 'open'
|
|
76
|
+
},
|
|
77
|
+
response => response.data.filter(issue => !('pull_request' in issue))
|
|
78
|
+
),
|
|
79
|
+
|
|
80
|
+
// closed issues, updated since syncClosedSince
|
|
81
|
+
octokit.paginate(
|
|
82
|
+
octokit.rest.issues.listForRepo,
|
|
83
|
+
{
|
|
84
|
+
...params,
|
|
85
|
+
state: 'closed',
|
|
86
|
+
since: new Date(syncClosedSince).toISOString()
|
|
87
|
+
},
|
|
88
|
+
response => response.data.filter(issue => !('pull_request' in issue))
|
|
89
|
+
),
|
|
90
|
+
|
|
91
|
+
// open pulls, all
|
|
92
|
+
octokit.paginate(
|
|
93
|
+
octokit.rest.pulls.list,
|
|
94
|
+
{
|
|
95
|
+
...params,
|
|
96
|
+
state: 'open'
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
|
|
100
|
+
// closed pulls, updated since syncClosedSince
|
|
101
|
+
octokit.paginate(
|
|
102
|
+
octokit.rest.pulls.list,
|
|
103
|
+
{
|
|
104
|
+
...params,
|
|
105
|
+
state: 'closed'
|
|
106
|
+
},
|
|
107
|
+
(response, done) => {
|
|
108
|
+
|
|
109
|
+
const pulls = response.data;
|
|
110
|
+
|
|
111
|
+
const filtered = pulls.filter(
|
|
112
|
+
pull => new Date(pull.updated_at).getTime() > syncClosedSince
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (filtered.length !== pulls.length) {
|
|
116
|
+
done();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return filtered;
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
open_issues,
|
|
126
|
+
closed_issues,
|
|
127
|
+
open_pull_requests,
|
|
128
|
+
closed_pull_requests
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// api ///////////////////
|
|
134
|
+
|
|
135
|
+
this.getInstallations = getInstallations;
|
|
136
|
+
|
|
137
|
+
this.getInstallationRepositories = getInstallationRepositories;
|
|
138
|
+
|
|
139
|
+
this.getRepositoryIssuesAndPulls = getRepositoryIssuesAndPulls;
|
|
140
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import BackgroundSync from './BackgroundSync.js';
|
|
2
|
+
import BackgroundSyncBackend from './BackgroundSyncBackend.js';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
__init__: [ 'backgroundSync' ],
|
|
5
|
-
backgroundSync: [ 'type', BackgroundSync ]
|
|
6
|
+
backgroundSync: [ 'type', BackgroundSync ],
|
|
7
|
+
backgroundSyncBackend: [ 'type', BackgroundSyncBackend ]
|
|
6
8
|
};
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
filterRepository,
|
|
6
6
|
getIdentifier,
|
|
7
7
|
getKey
|
|
8
|
-
} from '
|
|
8
|
+
} from '../../filters.js';
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
*
|
|
14
14
|
* @constructor
|
|
15
15
|
*
|
|
16
|
-
* @param {import('
|
|
17
|
-
* @param {import('
|
|
18
|
-
* @param {import('
|
|
16
|
+
* @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
|
|
17
|
+
* @param {import('../../store.js').default} store
|
|
18
|
+
* @param {import('../../types.js').Logger} logger
|
|
19
19
|
*/
|
|
20
20
|
export default function EventsSync(webhookEvents, store, logger) {
|
|
21
21
|
|
|
22
22
|
const log = logger.child({
|
|
23
|
-
name: 'wuffle:
|
|
23
|
+
name: 'wuffle:events-sync'
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
// issues /////////////////////
|
|
@@ -104,6 +104,8 @@ export default function EventsSync(webhookEvents, store, logger) {
|
|
|
104
104
|
'pull_request.converted_to_draft',
|
|
105
105
|
'pull_request.assigned',
|
|
106
106
|
'pull_request.unassigned',
|
|
107
|
+
'pull_request.milestoned',
|
|
108
|
+
'pull_request.demilestoned',
|
|
107
109
|
'pull_request.synchronize',
|
|
108
110
|
'pull_request.closed',
|
|
109
111
|
'pull_request.review_requested',
|
|
@@ -28,6 +28,7 @@ const RequiredEvents = [
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @typedef {import('./types.js').Installation} Installation
|
|
31
|
+
* @typedef {import('../../types.js').Octokit} Octokit
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -53,8 +54,19 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
53
54
|
|
|
54
55
|
// cached data //////////////////
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @type {Promise<Installation[]> | null}
|
|
59
|
+
*/
|
|
56
60
|
let installations = null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @type {Promise<Record<string, Installation>> | null}
|
|
64
|
+
*/
|
|
57
65
|
let installationsByLogin = null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @type {Promise<Record<string, Installation>> | null}
|
|
69
|
+
*/
|
|
58
70
|
let installationsById = null;
|
|
59
71
|
|
|
60
72
|
// reactivity ////////////////////
|
|
@@ -71,12 +83,17 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
71
83
|
// functionality /////////////////
|
|
72
84
|
|
|
73
85
|
/**
|
|
74
|
-
* @return {Promise<
|
|
86
|
+
* @return {Promise<Octokit>}
|
|
75
87
|
*/
|
|
76
88
|
function getAppScopedClient() {
|
|
77
89
|
return app.auth();
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Return installations
|
|
94
|
+
*
|
|
95
|
+
* @return {Promise<Installation[]>}
|
|
96
|
+
*/
|
|
80
97
|
function getInstallations() {
|
|
81
98
|
installations = installations || fetchInstallations().then(installations => {
|
|
82
99
|
|
|
@@ -90,6 +107,11 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
90
107
|
return installations;
|
|
91
108
|
}
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Return installations by ID
|
|
112
|
+
*
|
|
113
|
+
* @return {Promise<Record<string, Installation>>}
|
|
114
|
+
*/
|
|
93
115
|
function getInstallationsById() {
|
|
94
116
|
|
|
95
117
|
installationsById = installationsById || getInstallations().then(
|
|
@@ -106,9 +128,9 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
106
128
|
/**
|
|
107
129
|
* Get an installation for the given id.
|
|
108
130
|
*
|
|
109
|
-
* @param {string} id
|
|
131
|
+
* @param {string|number} id
|
|
110
132
|
*
|
|
111
|
-
* @return {Promise<Installation
|
|
133
|
+
* @return {Promise<Installation | undefined>}
|
|
112
134
|
*/
|
|
113
135
|
function getInstallationById(id) {
|
|
114
136
|
return getInstallationsById().then(byId => {
|
|
@@ -119,7 +141,7 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
119
141
|
/**
|
|
120
142
|
* Return map of installations, grouped by org / login.
|
|
121
143
|
*
|
|
122
|
-
* @return {Promise<
|
|
144
|
+
* @return {Promise<Record<string, Installation>>}
|
|
123
145
|
*/
|
|
124
146
|
function getInstallationsByLogin() {
|
|
125
147
|
|
|
@@ -155,6 +177,13 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
155
177
|
});
|
|
156
178
|
}
|
|
157
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Return true if installation is enabled.
|
|
182
|
+
*
|
|
183
|
+
* @param {Installation} installation
|
|
184
|
+
*
|
|
185
|
+
* @return {Promise<boolean>}
|
|
186
|
+
*/
|
|
158
187
|
function isInstallationEnabled(installation) {
|
|
159
188
|
|
|
160
189
|
const {
|
|
@@ -240,10 +269,10 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
240
269
|
/**
|
|
241
270
|
* Fetch active installations.
|
|
242
271
|
*
|
|
243
|
-
* @return {Promise<
|
|
272
|
+
* @return {Promise<Installation[]>} installations
|
|
244
273
|
*/
|
|
245
274
|
function fetchInstallations() {
|
|
246
|
-
return /** @type Promise<
|
|
275
|
+
return /** @type Promise<Installation[]> */ (getAppScopedClient().then(
|
|
247
276
|
octokit => octokit.paginate(
|
|
248
277
|
octokit.rest.apps.listInstallations,
|
|
249
278
|
{ per_page: 100 }
|
|
@@ -253,13 +282,47 @@ export default function GithubApp(config, app, logger, injector) {
|
|
|
253
282
|
|
|
254
283
|
// api ///////////////////
|
|
255
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Return an application-scoped octokit client.
|
|
287
|
+
*
|
|
288
|
+
* @return {Promise<Octokit>}
|
|
289
|
+
*/
|
|
256
290
|
this.getAppScopedClient = getAppScopedClient;
|
|
257
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Return true if installation is enabled.
|
|
294
|
+
*
|
|
295
|
+
* @param {Installation} installation
|
|
296
|
+
*
|
|
297
|
+
* @return {Promise<boolean>}
|
|
298
|
+
*/
|
|
258
299
|
this.isInstallationEnabled = isInstallationEnabled;
|
|
259
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Get an installation for the given login.
|
|
303
|
+
*
|
|
304
|
+
* This method throws if an installation for the given login does not exist.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} login
|
|
307
|
+
*
|
|
308
|
+
* @return {Promise<Installation>}
|
|
309
|
+
*/
|
|
260
310
|
this.getInstallationByLogin = getInstallationByLogin;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get an installation for the given id.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} id
|
|
316
|
+
*
|
|
317
|
+
* @return {Promise<Installation | undefined>}
|
|
318
|
+
*/
|
|
261
319
|
this.getInstallationById = getInstallationById;
|
|
262
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Return installations
|
|
323
|
+
*
|
|
324
|
+
* @return {Promise<Installation[]>}
|
|
325
|
+
*/
|
|
263
326
|
this.getInstallations = getInstallations;
|
|
264
327
|
|
|
265
328
|
}
|
|
@@ -12,8 +12,16 @@ import gql from 'fake-tag';
|
|
|
12
12
|
* @param {import('../../events.js').default} events
|
|
13
13
|
* @param {import('../github-client/GithubClient.js').default} githubClient
|
|
14
14
|
* @param {import('../../store.js').default} store
|
|
15
|
+
* @param {import('../issue-filter/IssueFilter.js').default} issueFilter
|
|
16
|
+
* @param {import('../../types.js').Logger } logger
|
|
15
17
|
*/
|
|
16
|
-
export default function GithubComments(webhookEvents, events, githubClient, store) {
|
|
18
|
+
export default function GithubComments(webhookEvents, events, githubClient, store, issueFilter, logger) {
|
|
19
|
+
|
|
20
|
+
const log = logger.child({
|
|
21
|
+
name: 'wuffle:github-comments'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const ifEnabled = issueFilter.createWebhookFilter(log);
|
|
17
25
|
|
|
18
26
|
// issues /////////////////////
|
|
19
27
|
|
|
@@ -108,7 +116,7 @@ export default function GithubComments(webhookEvents, events, githubClient, stor
|
|
|
108
116
|
|
|
109
117
|
webhookEvents.on([
|
|
110
118
|
'issue_comment'
|
|
111
|
-
], async ({ payload }) => {
|
|
119
|
+
], ifEnabled(async ({ payload }) => {
|
|
112
120
|
const {
|
|
113
121
|
action,
|
|
114
122
|
comment: _comment,
|
|
@@ -169,7 +177,7 @@ export default function GithubComments(webhookEvents, events, githubClient, stor
|
|
|
169
177
|
...commentedIssue,
|
|
170
178
|
comments
|
|
171
179
|
});
|
|
172
|
-
});
|
|
180
|
+
}));
|
|
173
181
|
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -11,8 +11,16 @@ import { filterUser, filterPull } from '../../filters.js';
|
|
|
11
11
|
* @param {import('../../events.js').default} events
|
|
12
12
|
* @param {import('../github-client/GithubClient.js').default} githubClient
|
|
13
13
|
* @param {import('../../store.js').default} store
|
|
14
|
+
* @param {import('../issue-filter/IssueFilter.js').default} issueFilter
|
|
15
|
+
* @param {import('../../types.js').Logger } logger
|
|
14
16
|
*/
|
|
15
|
-
export default function GithubReviews(webhookEvents, events, githubClient, store) {
|
|
17
|
+
export default function GithubReviews(webhookEvents, events, githubClient, store, issueFilter, logger) {
|
|
18
|
+
|
|
19
|
+
const log = logger.child({
|
|
20
|
+
name: 'wuffle:github-reviews'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const ifEnabled = issueFilter.createWebhookFilter(log);
|
|
16
24
|
|
|
17
25
|
// issues /////////////////////
|
|
18
26
|
|
|
@@ -54,7 +62,7 @@ export default function GithubReviews(webhookEvents, events, githubClient, store
|
|
|
54
62
|
|
|
55
63
|
webhookEvents.on([
|
|
56
64
|
'pull_request_review'
|
|
57
|
-
], async ({ payload }) => {
|
|
65
|
+
], ifEnabled(async ({ payload }) => {
|
|
58
66
|
const {
|
|
59
67
|
action,
|
|
60
68
|
review: _review,
|
|
@@ -102,7 +110,7 @@ export default function GithubReviews(webhookEvents, events, githubClient, store
|
|
|
102
110
|
...pull_request,
|
|
103
111
|
reviews
|
|
104
112
|
});
|
|
105
|
-
});
|
|
113
|
+
}));
|
|
106
114
|
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -11,8 +11,16 @@ import { filterPull } from '../../filters.js';
|
|
|
11
11
|
* @param {import('../../events.js').default} events
|
|
12
12
|
* @param {import('../github-client/GithubClient.js').default} githubClient
|
|
13
13
|
* @param {import('../../store.js').default} store
|
|
14
|
+
* @param {import('../issue-filter/IssueFilter.js').default} issueFilter
|
|
15
|
+
* @param {import('../../types.js').Logger } logger
|
|
14
16
|
*/
|
|
15
|
-
export default function
|
|
17
|
+
export default function GithubStatuses(webhookEvents, events, githubClient, store, issueFilter, logger) {
|
|
18
|
+
|
|
19
|
+
const log = logger.child({
|
|
20
|
+
name: 'wuffle:github-statuses'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const ifEnabled = issueFilter.createWebhookFilter(log);
|
|
16
24
|
|
|
17
25
|
// issues /////////////////////
|
|
18
26
|
|
|
@@ -89,7 +97,7 @@ export default function GithubStates(webhookEvents, events, githubClient, store)
|
|
|
89
97
|
webhookEvents.on([
|
|
90
98
|
'pull_request.opened',
|
|
91
99
|
'pull_request.synchronize'
|
|
92
|
-
], async ({ payload }) => {
|
|
100
|
+
], ifEnabled(async ({ payload }) => {
|
|
93
101
|
|
|
94
102
|
const {
|
|
95
103
|
pull_request: _pull_request,
|
|
@@ -108,7 +116,7 @@ export default function GithubStates(webhookEvents, events, githubClient, store)
|
|
|
108
116
|
id,
|
|
109
117
|
statuses
|
|
110
118
|
});
|
|
111
|
-
});
|
|
119
|
+
}));
|
|
112
120
|
|
|
113
121
|
webhookEvents.on([
|
|
114
122
|
'status'
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* @typedef { { ignoreFilter?: string } } StoreFilterConfig
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { filterIssueOrPull } from '../../filters.js';
|
|
6
|
+
import { issueIdent } from '../../util/meta.js';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* An component that configures the store to filter certain elements,
|
|
7
10
|
* effectively making them invisible to the board and its users.
|
|
8
11
|
*
|
|
9
12
|
* @param { StoreFilterConfig } config
|
|
10
|
-
*
|
|
11
13
|
* @param { import('../../store.js').default } store
|
|
12
14
|
* @param { import('../search/Search.js').default } search
|
|
13
15
|
* @param { import('../../types.js').Logger } logger
|
|
@@ -21,21 +23,50 @@ export default function IssueFilter(config, store, search, logger) {
|
|
|
21
23
|
/**
|
|
22
24
|
* @type { import('../search/Search.js').FilterFn }
|
|
23
25
|
*/
|
|
24
|
-
let
|
|
26
|
+
let isIgnored = (issue) => false;
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
if ('ignoreFilter' in config) {
|
|
28
30
|
const ignoreFilterFn = search.buildFilterFn(config.ignoreFilter);
|
|
29
31
|
|
|
30
32
|
if (ignoreFilterFn) {
|
|
31
|
-
|
|
33
|
+
isIgnored = ignoreFilterFn;
|
|
32
34
|
|
|
33
|
-
store.setIgnoreFilter(
|
|
35
|
+
store.setIgnoreFilter(isIgnored);
|
|
34
36
|
} else {
|
|
35
37
|
log.warn('unparseable <ignoreFilter> - please correct your board configuration');
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('../../types.js').Logger } log
|
|
43
|
+
*
|
|
44
|
+
* @return { (filterFn) => (any) => any }
|
|
45
|
+
*/
|
|
46
|
+
function createWebhookFilter(log) {
|
|
47
|
+
|
|
48
|
+
return function ifEnabled(webhookHandlerFn) {
|
|
49
|
+
|
|
50
|
+
return (context) => {
|
|
51
|
+
|
|
52
|
+
const payload = context.payload;
|
|
53
|
+
|
|
54
|
+
const issueOrPull = filterIssueOrPull(
|
|
55
|
+
payload.issue || payload.pull_request,
|
|
56
|
+
payload.repository
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (isIgnored(issueOrPull)) {
|
|
60
|
+
log.debug({ issue: issueIdent(issueOrPull) }, 'issue matching ignore filter');
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return webhookHandlerFn(context);
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
39
70
|
/**
|
|
40
71
|
* Figure whether the issue shall be ignored (by ignore filter rules)
|
|
41
72
|
*
|
|
@@ -43,8 +74,12 @@ export default function IssueFilter(config, store, search, logger) {
|
|
|
43
74
|
*
|
|
44
75
|
* @return {boolean} true, if issue shall be ignored
|
|
45
76
|
*/
|
|
46
|
-
this.isIgnored =
|
|
47
|
-
return ignoreFilter(issue);
|
|
48
|
-
};
|
|
77
|
+
this.isIgnored = isIgnored;
|
|
49
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @param {import('../../types.js').Logger } log
|
|
81
|
+
*
|
|
82
|
+
* @return { (filterFn) => (any) => any }
|
|
83
|
+
*/
|
|
84
|
+
this.createWebhookFilter = createWebhookFilter;
|
|
50
85
|
}
|
package/lib/apps/log-events.js
CHANGED
|
@@ -27,18 +27,18 @@ export default function LogEvents(logger, webhookEvents) {
|
|
|
27
27
|
|
|
28
28
|
log.info('dumping webhook events to %s', path.resolve(eventsDir));
|
|
29
29
|
|
|
30
|
-
function write(
|
|
30
|
+
function write(name, payload) {
|
|
31
31
|
|
|
32
32
|
const {
|
|
33
33
|
action
|
|
34
34
|
} = payload;
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const eventName = action ? `${name}.${action}` : name;
|
|
37
37
|
|
|
38
|
-
const data = JSON.stringify({
|
|
38
|
+
const data = JSON.stringify({ name, payload }, null, ' ');
|
|
39
39
|
|
|
40
40
|
return fs.mkdir(eventsDir, { recursive: true }).then(() => {
|
|
41
|
-
const fileName = path.join(eventsDir, `${Date.now()}-${counter++}-${
|
|
41
|
+
const fileName = path.join(eventsDir, `${Date.now()}-${counter++}-${eventName}.json`);
|
|
42
42
|
|
|
43
43
|
return fs.writeFile(fileName, data, 'utf8');
|
|
44
44
|
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPackageVersion,
|
|
3
3
|
hash
|
|
4
|
-
} from '
|
|
4
|
+
} from '../../util/index.js';
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @constructor
|
|
9
9
|
*
|
|
10
10
|
* @param {any} config
|
|
11
|
-
* @param {import('
|
|
12
|
-
* @param {import('
|
|
13
|
-
* @param {import('
|
|
11
|
+
* @param {import('../../types.js').Logger} logger
|
|
12
|
+
* @param {import('../../events.js').default} events
|
|
13
|
+
* @param {import('../../store.js').default} store
|
|
14
14
|
*/
|
|
15
15
|
export default function ReindexStore(config, logger, events, store) {
|
|
16
16
|
|
|
@@ -48,6 +48,8 @@ export default function ReindexStore(config, logger, events, store) {
|
|
|
48
48
|
log.info('config changed');
|
|
49
49
|
|
|
50
50
|
await reindexStore();
|
|
51
|
+
} else {
|
|
52
|
+
log.debug('config unchanged - skipping reindexing');
|
|
51
53
|
}
|
|
52
54
|
});
|
|
53
55
|
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const appModules = [
|
|
|
11
11
|
? import('./apps/dump-store/s3/index.js')
|
|
12
12
|
: import('./apps/dump-store/local/index.js')
|
|
13
13
|
),
|
|
14
|
-
import('./apps/events-sync.js'),
|
|
14
|
+
import('./apps/events-sync/index.js'),
|
|
15
15
|
import('./apps/github-app/index.js'),
|
|
16
16
|
import('./apps/github-issues/index.js'),
|
|
17
17
|
import('./apps/github-comments/index.js'),
|
|
@@ -24,11 +24,11 @@ const appModules = [
|
|
|
24
24
|
import('./apps/user-access/index.js'),
|
|
25
25
|
import('./apps/search/index.js'),
|
|
26
26
|
import('./apps/background-sync/index.js'),
|
|
27
|
-
import('./apps/automatic-dev-flow.js'),
|
|
27
|
+
import('./apps/automatic-dev-flow/index.js'),
|
|
28
28
|
import('./apps/auth-routes/index.js'),
|
|
29
29
|
import('./apps/board-api-routes/index.js'),
|
|
30
30
|
import('./apps/board-routes.js'),
|
|
31
|
-
import('./apps/reindex-store.js')
|
|
31
|
+
import('./apps/reindex-store/index.js')
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
import loadConfig from './load-config.js';
|
|
@@ -78,17 +78,25 @@ export default function Wuffle(app, { getRouter }) {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
const coreModule = {
|
|
81
|
-
'app': [ 'value', app ],
|
|
82
81
|
'config': [ 'value', config ],
|
|
83
|
-
'router': [ 'value', router ],
|
|
84
82
|
'logger': [ 'value', logger ],
|
|
85
83
|
'columns': [ 'type', Columns ],
|
|
86
84
|
'store': [ 'type', Store ],
|
|
87
85
|
'events': [ 'value', events ]
|
|
88
86
|
};
|
|
89
87
|
|
|
88
|
+
const webModule = {
|
|
89
|
+
'router': [ 'value', router ]
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const probotModule = {
|
|
93
|
+
'app': [ 'value', app ],
|
|
94
|
+
};
|
|
95
|
+
|
|
90
96
|
const injector = new AsyncInjector([
|
|
91
97
|
coreModule,
|
|
98
|
+
probotModule,
|
|
99
|
+
webModule,
|
|
92
100
|
...modules
|
|
93
101
|
]);
|
|
94
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wuffle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "A multi-repository task board for GitHub issues",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nico Rehwaldt",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"all": "run-s lint test",
|
|
36
36
|
"dev": "nodemon",
|
|
37
37
|
"start": "NODE_ENV=production node ./bin/wuffle",
|
|
38
|
-
"test": "mocha 'test/**/*.js' --exit",
|
|
38
|
+
"test": "mocha -r test/helpers/globals.js 'test/**/*.js' --exit",
|
|
39
39
|
"lint": "run-s lint:*",
|
|
40
40
|
"lint:eslint": "eslint .",
|
|
41
41
|
"lint:types": "tsc --pretty",
|
|
@@ -61,13 +61,14 @@
|
|
|
61
61
|
"@types/compression": "^1.8.1",
|
|
62
62
|
"@types/express-session": "^1.19.0",
|
|
63
63
|
"@types/mocha": "^10.0.10",
|
|
64
|
+
"@types/sinon": "^21.0.1",
|
|
64
65
|
"chai": "^6.2.2",
|
|
65
66
|
"graphql": "^16.13.2",
|
|
66
67
|
"mocha": "^11.7.5",
|
|
67
68
|
"nock": "^14.0.13",
|
|
68
69
|
"nodemon": "^3.1.14",
|
|
69
70
|
"npm-run-all2": "^8.0.4",
|
|
70
|
-
"sinon": "^
|
|
71
|
+
"sinon": "^22.0.0",
|
|
71
72
|
"sinon-chai": "^4.0.0",
|
|
72
73
|
"typescript": "^5.9.3"
|
|
73
74
|
},
|
|
@@ -81,5 +82,6 @@
|
|
|
81
82
|
"app.yml",
|
|
82
83
|
"index.js",
|
|
83
84
|
"wuffle.config.example.js"
|
|
84
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"gitHead": "ea9ac74526b02f9834b5e926df0e8e82536eba07"
|
|
85
87
|
}
|