wuffle 0.73.3 → 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.
@@ -12,11 +12,11 @@ const CHANGES_REQUESTED = 'changes_requested';
12
12
  *
13
13
  * @constructor
14
14
  *
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
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
20
20
  */
21
21
  export default function(webhookEvents, githubIssues, columns, issueFilter, logger) {
22
22
 
@@ -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,
@@ -465,7 +398,7 @@ We automatically synchronize all repositories you granted us access to via the G
465
398
  log.info('start');
466
399
 
467
400
  try {
468
- const installations = await githubApp.getInstallations();
401
+ const installations = await backgroundSyncBackend.getInstallations();
469
402
 
470
403
  await doSync(installations);
471
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
  }
@@ -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.3",
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
  },
@@ -82,5 +83,5 @@
82
83
  "index.js",
83
84
  "wuffle.config.example.js"
84
85
  ],
85
- "gitHead": "1d7cdf09524102d300d6230b840959cdd1479e3e"
86
+ "gitHead": "ea9ac74526b02f9834b5e926df0e8e82536eba07"
86
87
  }