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 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('./webhook-events/WebhookEvents.js').default} webhookEvents
19
- * @param {import('./github-issues/GithubIssues.js').default} githubIssues
20
- * @param {import('../columns.js').default} columns
21
- * @param {import('./issue-filter/IssueFilter.js').default} issueFilter
22
- * @param {import('../types.js').Logger } logger
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
- function ifEnabled(webhookHandlerFn) {
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',
@@ -0,0 +1,6 @@
1
+ import AutomaticDevFlow from './AutomaticDevFlow.js';
2
+
3
+ export default {
4
+ __init__: [ 'automaticDevFlow' ],
5
+ automaticDevFlow: [ 'type', AutomaticDevFlow ]
6
+ };
@@ -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(logger, config, store, githubClient, githubApp, events) {
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 octokit = await githubClient.getOrgScoped(owner);
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
- const owner = repository.owner.login;
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 params = {
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
- ] = await Promise.all([
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
- const {
221
- id
222
- } = update;
153
+ foundIssues[update.id] = update;
223
154
 
224
- const existingIssue = await store.getIssueById(id);
225
-
226
- if (existingIssue && existingIssue.updated_at >= update.updated_at) {
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
- await Promise.all(pendingUpdates.map(update => store.updateIssue(update)));
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 found issues
328
+ // emit background sync event for all updated issues
401
329
 
402
330
  await syncDetails(
403
- Object.keys(foundIssues),
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(issueIds, syncClosedSince, syncOpenSince) {
368
+ function syncDetails(updatedIssues, syncClosedSince, syncOpenSince) {
369
+
370
+ const jobs = updatedIssues.map(async updatedIssue => {
441
371
 
442
- const jobs = issueIds.map(async id => {
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 githubApp.getInstallations();
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 '../filters.js';
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('./webhook-events/WebhookEvents.js').default} webhookEvents
17
- * @param {import('../store.js').default} store
18
- * @param {import('../types.js').Logger} logger
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:user-access'
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',
@@ -0,0 +1,6 @@
1
+ import EventsSync from './EventsSync.js';
2
+
3
+ export default {
4
+ __init__: [ 'eventsSync' ],
5
+ eventsSync: [ 'type', EventsSync ]
6
+ };
@@ -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<import('../../types.js').Octokit>}
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<Object<String, Installation>>}
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<Array<Installation>>} installations
272
+ * @return {Promise<Installation[]>} installations
244
273
  */
245
274
  function fetchInstallations() {
246
- return /** @type Promise<Array<Installation>> */ (getAppScopedClient().then(
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 GithubStates(webhookEvents, events, githubClient, store) {
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 ignoreFilter = (issue) => false;
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
- ignoreFilter = ignoreFilterFn;
33
+ isIgnored = ignoreFilterFn;
32
34
 
33
- store.setIgnoreFilter(ignoreFilter);
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 = function(issue) {
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
  }
@@ -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(event, payload) {
30
+ function write(name, payload) {
31
31
 
32
32
  const {
33
33
  action
34
34
  } = payload;
35
35
 
36
- const name = action ? `${event}.${action}` : event;
36
+ const eventName = action ? `${name}.${action}` : name;
37
37
 
38
- const data = JSON.stringify({ event, payload }, null, ' ');
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++}-${name}.json`);
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 '../util/index.js';
4
+ } from '../../util/index.js';
5
5
 
6
6
 
7
7
  /**
8
8
  * @constructor
9
9
  *
10
10
  * @param {any} config
11
- * @param {import('../types.js').Logger} logger
12
- * @param {import('../events.js').default} events
13
- * @param {import('../store.js').default} store
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
 
@@ -0,0 +1,6 @@
1
+ import ReindexStore from './ReindexStore.js';
2
+
3
+ export default {
4
+ __init__: [ 'reindexStore' ],
5
+ reindexStore: [ 'type', ReindexStore ]
6
+ };
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.73.2",
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": "^21.1.2",
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
  }