wuffle 0.41.5 → 0.43.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.
Files changed (41) hide show
  1. package/bin/run.js +1 -1
  2. package/lib/apps/auth-routes/AuthRoutes.js +5 -3
  3. package/lib/apps/automatic-dev-flow.js +2 -0
  4. package/lib/apps/background-sync/BackgroundSync.js +11 -9
  5. package/lib/apps/board-api-routes.js +3 -3
  6. package/lib/apps/board-routes.js +1 -1
  7. package/lib/apps/dump-store/local/DumpStoreLocal.js +5 -3
  8. package/lib/apps/dump-store/s3/DumpStoreS3.js +5 -3
  9. package/lib/apps/events-sync.js +28 -18
  10. package/lib/apps/github-app/GithubApp.js +19 -3
  11. package/lib/apps/github-checks/GithubChecks.js +6 -4
  12. package/lib/apps/github-client/GithubClient.js +33 -3
  13. package/lib/apps/github-comments/GithubComments.js +205 -0
  14. package/lib/apps/github-comments/index.js +6 -0
  15. package/lib/apps/github-issues/GithubIssues.js +77 -22
  16. package/lib/apps/github-reviews/GithubReviews.js +7 -3
  17. package/lib/apps/github-statuses/GithubStatuses.js +2 -0
  18. package/lib/apps/log-events.js +11 -7
  19. package/lib/apps/reindex-store.js +14 -2
  20. package/lib/apps/route-compression.js +1 -1
  21. package/lib/apps/route-https.js +1 -1
  22. package/lib/apps/search/Search.js +32 -5
  23. package/lib/apps/security-context/SecurityContext.js +5 -0
  24. package/lib/apps/user-access/UserAccess.js +6 -4
  25. package/lib/apps/webhook-events/WebhookEvents.js +33 -9
  26. package/lib/columns.js +16 -3
  27. package/lib/filters.js +20 -16
  28. package/lib/index.js +1 -0
  29. package/lib/links.js +15 -5
  30. package/lib/probot/apps/setup.js +1 -1
  31. package/lib/probot/manifest-creation.js +1 -1
  32. package/lib/store.js +11 -0
  33. package/lib/types.d.ts +23 -6
  34. package/lib/util/index.js +1 -1
  35. package/lib/util/links.js +48 -26
  36. package/package.json +24 -19
  37. package/public/bundle.css +1 -1
  38. package/public/bundle.js +1 -1
  39. package/public/bundle.js.map +1 -1
  40. package/public/service-worker.js.map +1 -1
  41. package/CHANGELOG.md +0 -465
@@ -8,13 +8,19 @@ const {
8
8
  } = linkTypes;
9
9
 
10
10
 
11
+ /**
12
+ * @constructor
13
+ *
14
+ * @param {import('../../types').Logger} logger
15
+ * @param {any} config
16
+ * @param {import('../../columns')} columns
17
+ */
11
18
  function GithubIssues(logger, config, columns) {
12
19
 
13
20
  const log = logger.child({
14
21
  name: 'wuffle:github-issues'
15
22
  });
16
23
 
17
-
18
24
  function getAssigneeUpdate(issue, newAssignee) {
19
25
 
20
26
  if (!newAssignee) {
@@ -49,27 +55,25 @@ function GithubIssues(logger, config, columns) {
49
55
  };
50
56
  }
51
57
 
58
+ return update;
59
+ }
60
+
61
+ function getLabelUpdate(issue, newColumn) {
62
+
52
63
  const issueLabels = issue.labels.map(l => l.name);
53
64
 
54
65
  const newLabel = newColumn.label;
55
66
 
56
- const labelsToAdd = (!newLabel || issueLabels.includes(newLabel)) ? [] : [ newLabel ];
67
+ const addLabels = (!newLabel || issueLabels.includes(newLabel)) ? [] : [ newLabel ];
57
68
 
58
- const labelsToRemove = columns.getAll().map(c => c.label).filter(
69
+ const removeLabels = columns.getAll().map(c => c.label).filter(
59
70
  label => label && label !== newLabel && issueLabels.includes(label)
60
71
  );
61
72
 
62
- if (labelsToRemove.length || labelsToAdd.length) {
63
- update = {
64
- ...update,
65
- labels: [
66
- ...issueLabels.filter(l => !labelsToRemove.includes(l)),
67
- ...labelsToAdd
68
- ]
69
- };
70
- }
71
-
72
- return update;
73
+ return {
74
+ addLabels,
75
+ removeLabels
76
+ };
73
77
  }
74
78
 
75
79
  function findIssue(context, issue_number) {
@@ -138,18 +142,67 @@ function GithubIssues(logger, config, columns) {
138
142
  ...getStateUpdate(issue, newColumn)
139
143
  };
140
144
 
141
- if (!hasKeys(update)) {
142
- return;
145
+ const {
146
+ addLabels,
147
+ removeLabels
148
+ } = getLabelUpdate(issue, newColumn);
149
+
150
+ const invocations = [];
151
+
152
+ if (hasKeys(addLabels)) {
153
+
154
+ const addLabelParams = context.repo({
155
+ issue_number,
156
+ labels: addLabels
157
+ });
158
+
159
+ log.info(addLabelParams, 'add labels');
160
+
161
+ invocations.push(
162
+ context.octokit.issues.addLabels(addLabelParams)
163
+ );
143
164
  }
144
165
 
145
- const params = context.repo({
146
- issue_number,
147
- ...update
148
- });
166
+ if (hasKeys(update)) {
149
167
 
150
- log.info(params, 'update');
168
+ const params = context.repo({
169
+ issue_number,
170
+ ...update
171
+ });
172
+
173
+ log.info(params, 'update');
151
174
 
152
- return context.octokit.issues.update(params);
175
+ invocations.push(
176
+ context.octokit.issues.update(params)
177
+ );
178
+ }
179
+
180
+ for (const label of removeLabels) {
181
+
182
+ const removeLabelParams = context.repo({
183
+ issue_number,
184
+ name: label
185
+ });
186
+
187
+ log.info({
188
+ ...removeLabelParams,
189
+ label
190
+ }, 'remove label');
191
+
192
+ invocations.push(
193
+ context.octokit.issues.removeLabel(removeLabelParams).catch(err => {
194
+
195
+ // gracefully handle non-existing label
196
+ // may have been already removed by other
197
+ // integrations
198
+ if (err.status !== 404) {
199
+ return Promise.reject(err);
200
+ }
201
+ })
202
+ );
203
+ }
204
+
205
+ return Promise.all(invocations);
153
206
  }
154
207
 
155
208
 
@@ -163,6 +216,8 @@ function GithubIssues(logger, config, columns) {
163
216
 
164
217
  this.getAssigneeUpdate = getAssigneeUpdate;
165
218
 
219
+ this.getLabelUpdate = getLabelUpdate;
220
+
166
221
  this.findAndMoveIssue = findAndMoveIssue;
167
222
 
168
223
  }
@@ -11,7 +11,9 @@ const {
11
11
  /**
12
12
  * This component updates the stored issues based on GitHub events.
13
13
  *
14
- * @param {import("../webhook-events/WebhookEvents")} webhookEvents
14
+ * @constructor
15
+ *
16
+ * @param {import('../webhook-events/WebhookEvents')} webhookEvents
15
17
  * @param {import('../../events')} events
16
18
  * @param {import('../github-client/GithubClient')} githubClient
17
19
  * @param {import('../../store')} store
@@ -120,7 +122,8 @@ function filterReview(review) {
120
122
  commit_id,
121
123
  submitted_at,
122
124
  state,
123
- user
125
+ user,
126
+ html_url
124
127
  } = review;
125
128
 
126
129
  return {
@@ -130,6 +133,7 @@ function filterReview(review) {
130
133
  commit_id,
131
134
  submitted_at,
132
135
  state: state.toLowerCase(),
133
- user: filterUser(user)
136
+ user: filterUser(user),
137
+ html_url
134
138
  };
135
139
  }
@@ -11,6 +11,8 @@ const {
11
11
  /**
12
12
  * This component updates synchronizes GitHub statuses with pull requests.
13
13
  *
14
+ * @constructor
15
+ *
14
16
  * @param {import('../webhook-events/WebhookEvents')} webhookEvents
15
17
  * @param {import('../../events')} events
16
18
  * @param {import('../github-client/GithubClient')} githubClient
@@ -8,10 +8,12 @@ const fs = require('fs').promises;
8
8
  * This component adds the #onActive method to subscribe to events
9
9
  * for explicitly activated repositories only.
10
10
  *
11
- * @param {import("../types").Logger} logger
12
- * @param {import("./webhook-events/WebhookEvents")} webhookEvents
11
+ * @constructor
12
+ *
13
+ * @param {import('../types').Logger} logger
14
+ * @param {import('./webhook-events/WebhookEvents')} webhookEvents
13
15
  */
14
- module.exports = function(logger, webhookEvents) {
16
+ function LogEvents(logger, webhookEvents) {
15
17
 
16
18
  if (process.env.NODE_ENV !== 'development' && !process.env.LOG_WEBHOOK_EVENTS) {
17
19
  return;
@@ -47,15 +49,17 @@ module.exports = function(logger, webhookEvents) {
47
49
 
48
50
  // behavior //////////////////////
49
51
 
50
- webhookEvents.on('*', async context => {
52
+ webhookEvents.onAny(async context => {
51
53
  const {
52
- event,
54
+ name,
53
55
  payload
54
56
  } = context;
55
57
 
56
- write(event, payload).catch(err => {
58
+ write(name, payload).catch(err => {
57
59
  log.error(err, 'failed to log event');
58
60
  });
59
61
  });
60
62
 
61
- };
63
+ }
64
+
65
+ module.exports = LogEvents;
@@ -2,7 +2,16 @@ const {
2
2
  version
3
3
  } = require('../../package.json');
4
4
 
5
- module.exports = function ReindexStore(logger, config, events, store) {
5
+
6
+ /**
7
+ * @constructor
8
+ *
9
+ * @param {any} logger
10
+ * @param {any} config
11
+ * @param {any} events
12
+ * @param {any} store
13
+ */
14
+ function ReindexStore(logger, config, events, store) {
6
15
 
7
16
  const log = logger.child({
8
17
  name: 'wuffle:reindex-store'
@@ -49,7 +58,10 @@ module.exports = function ReindexStore(logger, config, events, store) {
49
58
  data.wuffleVersion = version;
50
59
  });
51
60
 
52
- };
61
+ }
62
+
63
+ module.exports = ReindexStore;
64
+
53
65
 
54
66
  // helpers //////////////////
55
67
 
@@ -3,7 +3,7 @@ const compression = require('compression');
3
3
  /**
4
4
  * Enables compression for routes
5
5
  *
6
- * @param {import("express").Router} router
6
+ * @param {import('express').Router} router
7
7
  */
8
8
  module.exports = function(router) {
9
9
 
@@ -5,7 +5,7 @@ const {
5
5
  /**
6
6
  * Enables https redirects and strict transport security.
7
7
  *
8
- * @param {import("express").Router} router
8
+ * @param {import('express').Router} router
9
9
  */
10
10
  module.exports = function(router) {
11
11
 
@@ -11,8 +11,10 @@ const {
11
11
  /**
12
12
  * This app allows you to create a search filter from a given term.
13
13
  *
14
- * @param {import("../../types").Logger} logger
15
- * @param {import("../../store")} store
14
+ * @constructor
15
+ *
16
+ * @param {import('../../types').Logger} logger
17
+ * @param {import('../../store')} store
16
18
  */
17
19
  function Search(logger, store) {
18
20
 
@@ -181,13 +183,38 @@ function Search(logger, store) {
181
183
  };
182
184
  },
183
185
 
186
+ commented: function commentedFilter(name) {
187
+
188
+ return function filterCommented(issue) {
189
+
190
+ const {
191
+ comments
192
+ } = issue;
193
+
194
+ // issues do not have comments attached
195
+ if (!Array.isArray(comments)) {
196
+ return false;
197
+ }
198
+
199
+ return (
200
+ comments.some(comment => fuzzyMatches(comment.user.login, name))
201
+ );
202
+ };
203
+ },
204
+
184
205
  involves: function involvesFilter(name) {
185
206
 
207
+ const isAssigned = filters.assignee(name);
208
+ const isAuthor = filters.author(name);
209
+ const isReviewer = filters.reviewer(name);
210
+ const hasCommented = filters.commented(name);
211
+
186
212
  return function filterInvolves(issue) {
187
213
  return (
188
- filters.assignee(name)(issue) ||
189
- filters.author(name)(issue) ||
190
- filters.reviewer(name)(issue)
214
+ isAssigned(issue) ||
215
+ isAuthor(issue) ||
216
+ isReviewer(issue) ||
217
+ hasCommented(issue)
191
218
  );
192
219
  };
193
220
  },
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @constructor
3
+ *
4
+ * @param {import('../github-client/GithubClient')} githubClient
5
+ */
1
6
  function SecurityContext(githubClient) {
2
7
 
3
8
  /**
@@ -8,10 +8,12 @@ const TTL = 1000 * 60 * 60 * 24 * 9;
8
8
  * This component provides the functionality to filter
9
9
  * issues based on user views.
10
10
  *
11
- * @param {import("../../types").Logger} logger
12
- * @param {import("../github-client/GithubClient")} githubClient
13
- * @param {import("../../events")} events
14
- * @param {import("../webhook-events/WebhookEvents")} webhookEvents
11
+ * @constructor
12
+ *
13
+ * @param {import('../../types').Logger} logger
14
+ * @param {import('../github-client/GithubClient')} githubClient
15
+ * @param {import('../../events')} events
16
+ * @param {import('../webhook-events/WebhookEvents')} webhookEvents
15
17
  */
16
18
  function UserAccess(logger, githubClient, events, webhookEvents) {
17
19
 
@@ -1,15 +1,18 @@
1
+ /**
2
+ * @constructor
3
+ *
4
+ * @param {import('../../types').ProbotApp} app
5
+ * @param {import('../github-app/GithubApp')} githubApp
6
+ */
1
7
  function WebhookEvents(app, githubApp) {
2
8
 
3
9
  /**
4
- * Register a event lister for a single
5
- * or a number of webhook events.
6
- *
7
- * @param {String|Array<String>} events
8
- * @param {Function} fn listener
10
+ * @template {Function} T
11
+ * @param {T} fn
9
12
  */
10
- function on(events, fn) {
11
- app.on(events, async context => {
13
+ function ifEnabled(fn) {
12
14
 
15
+ return async (context) => {
13
16
  const {
14
17
  payload
15
18
  } = context;
@@ -27,13 +30,34 @@ function WebhookEvents(app, githubApp) {
27
30
  }
28
31
 
29
32
  return fn(context);
30
- });
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Register a event lister for a single
38
+ * or a number of webhook events.
39
+ *
40
+ * @param {any|any[]} events
41
+ * @param {Function} fn listener
42
+ */
43
+ function on(events, fn) {
44
+ app.on(events, ifEnabled(fn));
45
+ }
46
+
47
+ /**
48
+ * Register an event listener for all
49
+ * webhook events.
50
+ *
51
+ * @param {Function} fn
52
+ */
53
+ function onAny(fn) {
54
+ app.onAny(ifEnabled(fn));
31
55
  }
32
56
 
33
57
  // api /////////////////
34
58
 
35
59
  this.on = on;
36
-
60
+ this.onAny = onAny;
37
61
  }
38
62
 
39
63
  module.exports = WebhookEvents;
package/lib/columns.js CHANGED
@@ -14,9 +14,15 @@ const StateToNames = {
14
14
  EXTERNAL_CONTRIBUTION: 'Inbox'
15
15
  };
16
16
 
17
+ /**
18
+ * @typedef { { name: string, label?: string|null, closed?: boolean } } ColumnDefinition
19
+ */
17
20
 
18
21
  class Columns {
19
22
 
23
+ /**
24
+ * @param { ColumnDefinition[] } columns
25
+ */
20
26
  constructor(columns) {
21
27
  this.columns = columns;
22
28
 
@@ -89,7 +95,11 @@ function createColumnGetter(columns) {
89
95
 
90
96
  return function(issue) {
91
97
 
92
- const column = sortedColumns.find(column => {
98
+ const {
99
+ column: issueColumn
100
+ } = issue;
101
+
102
+ const columns = sortedColumns.filter(column => {
93
103
 
94
104
  const issueClosed = issue.state === 'closed';
95
105
 
@@ -101,14 +111,17 @@ function createColumnGetter(columns) {
101
111
  return false;
102
112
  }
103
113
 
104
- if (!columnLabel) {
114
+ if (!columnLabel && defaultColumn !== column) {
105
115
  return true;
106
116
  }
107
117
 
108
118
  return issue.labels.find(l => l.name === columnLabel);
109
119
  });
110
120
 
111
- return (column || defaultColumn);
121
+ // ensure that column is stable; it only changes
122
+ // if the given configuration doesn't match the
123
+ // existing column
124
+ return columns.find(c => c.name === issueColumn) || columns[0] || defaultColumn;
112
125
  };
113
126
  }
114
127
 
package/lib/filters.js CHANGED
@@ -31,7 +31,8 @@ function filterRepository(githubRepository) {
31
31
  node_id,
32
32
  name,
33
33
  'private': isPrivate,
34
- owner
34
+ owner,
35
+ html_url
35
36
  } = githubRepository;
36
37
 
37
38
  return {
@@ -39,7 +40,8 @@ function filterRepository(githubRepository) {
39
40
  node_id,
40
41
  name,
41
42
  'private': isPrivate,
42
- owner: filterUser(owner)
43
+ owner: filterUser(owner),
44
+ html_url
43
45
  };
44
46
  }
45
47
 
@@ -52,14 +54,16 @@ function filterUser(githubUser) {
52
54
  id,
53
55
  node_id,
54
56
  login,
55
- avatar_url
57
+ avatar_url,
58
+ html_url
56
59
  } = githubUser;
57
60
 
58
61
  return {
59
62
  id,
60
63
  node_id,
61
64
  login,
62
- avatar_url
65
+ avatar_url,
66
+ html_url
63
67
  };
64
68
  }
65
69
 
@@ -112,7 +116,8 @@ function filterMilestone(githubMilestone) {
112
116
  node_id,
113
117
  number,
114
118
  title,
115
- state
119
+ state,
120
+ html_url
116
121
  } = githubMilestone;
117
122
 
118
123
  return {
@@ -120,7 +125,8 @@ function filterMilestone(githubMilestone) {
120
125
  node_id,
121
126
  number,
122
127
  title,
123
- state
128
+ state,
129
+ html_url
124
130
  };
125
131
  }
126
132
 
@@ -153,12 +159,11 @@ function filterPull(githubPull, githubRepository) {
153
159
  mergeable,
154
160
  mergeable_state,
155
161
  merged_by,
156
- comments,
157
- review_comments,
158
162
  commits,
159
163
  additions,
160
164
  deletions,
161
- changed_files
165
+ changed_files,
166
+ html_url
162
167
  } = githubPull;
163
168
 
164
169
  // stable ID that is independent from GitHubs internal issue/pr distinction
@@ -192,14 +197,13 @@ function filterPull(githubPull, githubRepository) {
192
197
  mergeable,
193
198
  mergeable_state,
194
199
  merged_by,
195
- comments,
196
- review_comments,
197
200
  commits,
198
201
  additions,
199
202
  deletions,
200
203
  changed_files,
201
204
  pull_request: true,
202
- repository: filterRepository(githubRepository)
205
+ repository: filterRepository(githubRepository),
206
+ html_url
203
207
  };
204
208
  }
205
209
 
@@ -221,8 +225,8 @@ function filterIssue(githubIssue, githubRepository) {
221
225
  assignees,
222
226
  labels,
223
227
  milestone,
224
- comments,
225
- pull_request
228
+ pull_request,
229
+ html_url
226
230
  } = githubIssue;
227
231
 
228
232
  // stable ID that is independent from GitHubs internal issue/pr distinction
@@ -249,9 +253,9 @@ function filterIssue(githubIssue, githubRepository) {
249
253
  assignees: assignees.map(filterUser),
250
254
  labels: labels.map(filterLabel),
251
255
  milestone: milestone ? filterMilestone(milestone) : null,
252
- comments,
253
256
  repository: filterRepository(githubRepository),
254
- pull_request: !!pull_request
257
+ pull_request: !!pull_request,
258
+ html_url
255
259
  };
256
260
 
257
261
  }
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@ const apps = [
15
15
  require('./apps/events-sync'),
16
16
  require('./apps/github-app'),
17
17
  require('./apps/github-issues'),
18
+ require('./apps/github-comments'),
18
19
  require('./apps/github-client'),
19
20
  require('./apps/github-checks'),
20
21
  require('./apps/github-reviews'),
package/lib/links.js CHANGED
@@ -19,6 +19,9 @@ const InverseLinkTypes = {
19
19
  LINKED_TO: LinkTypes.LINKED_BY
20
20
  };
21
21
 
22
+ /**
23
+ * @typedef { { sourceId: string, targetId: string, type: string, key: string } } Link
24
+ */
22
25
 
23
26
  /**
24
27
  * A utility to maintain links
@@ -30,6 +33,13 @@ class Links {
30
33
  this.inverseLinks = (data && data.inverseLinks) || {};
31
34
  }
32
35
 
36
+ /**
37
+ * @param {string} sourceId
38
+ * @param {string} targetId
39
+ * @param {string} linkType
40
+ *
41
+ * @return {Link}
42
+ */
33
43
  createLink(sourceId, targetId, linkType) {
34
44
 
35
45
  const key = `${targetId}-${linkType}`;
@@ -47,7 +57,7 @@ class Links {
47
57
  /**
48
58
  * Establish a link between source and target with the given type.
49
59
  *
50
- * @param {Object} link
60
+ * @param {Link} link
51
61
  */
52
62
  addLink(link) {
53
63
 
@@ -92,9 +102,9 @@ class Links {
92
102
  /**
93
103
  * Get all links that have the issue as the source.
94
104
  *
95
- * @param {number} sourceId
105
+ * @param {string} sourceId
96
106
  *
97
- * @return {Array<Object>} links
107
+ * @return {Link[]} links
98
108
  */
99
109
  getBySource(sourceId) {
100
110
  const links = this.getDirect(sourceId);
@@ -118,9 +128,9 @@ class Links {
118
128
  /**
119
129
  * Remove primary links that have the issue as the source.
120
130
  *
121
- * @param {number} sourceId
131
+ * @param {string} sourceId
122
132
  *
123
- * @return {Object} removedLinks
133
+ * @return {Link[]} removedLinks
124
134
  */
125
135
  removeBySource(sourceId) {
126
136
 
@@ -91,7 +91,7 @@ function getBaseUrl(req) {
91
91
  }
92
92
 
93
93
 
94
- function renderSuccess(appUrl=null) {
94
+ function renderSuccess(appUrl = null) {
95
95
 
96
96
  return `
97
97
  <!DOCTYPE html>
@@ -17,7 +17,7 @@ class ManifestCreation extends ProbotManifestCreation {
17
17
  const options = {
18
18
  code,
19
19
  mediaType: {
20
- previews: ['fury'] // needed for GHES 2.20 and older
20
+ previews: [ 'fury' ] // needed for GHES 2.20 and older
21
21
  },
22
22
  ...(process.env.GHE_HOST && {
23
23
  baseUrl: `${process.env.GHE_PROTOCOL || 'https'}://${
package/lib/store.js CHANGED
@@ -15,8 +15,19 @@ const {
15
15
  const { Links } = require('./links');
16
16
 
17
17
 
18
+ /**
19
+ * The store that holds all board data
20
+ * and makes it accessible.
21
+ */
18
22
  class Store {
19
23
 
24
+ /**
25
+ * @param {import('./columns')} columns
26
+ * @param {import('./types').Logger} logger
27
+ * @param {import('./events')} events
28
+ *
29
+ * @constructor
30
+ */
20
31
  constructor(columns, logger, events) {
21
32
  this.log = logger.child({
22
33
  name: 'wuffle:store'