wuffle 0.74.0 → 0.75.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.
@@ -60,7 +60,7 @@ export default function AuthRoutes(logger, router, securityContext) {
60
60
  };
61
61
 
62
62
  const params = new URLSearchParams();
63
- params.append('client_id', process.env.GITHUB_CLIENT_ID);
63
+ params.append('client_id', /** @type {string} */ (process.env.GITHUB_CLIENT_ID));
64
64
  params.append('state', state);
65
65
  params.append('redirect_uri', appUrl('/wuffle/login/callback'));
66
66
 
@@ -81,10 +81,9 @@ export default function AuthRoutes(logger, router, securityContext) {
81
81
 
82
82
  log.debug({ session_id }, 'logging out');
83
83
 
84
- return req.session.destroy(function(err) {
85
- return res.redirect(redirectTo);
86
- }) && null;
87
-
84
+ req.session.destroy(function(err) {
85
+ res.redirect(redirectTo);
86
+ });
88
87
  });
89
88
 
90
89
 
@@ -128,8 +127,8 @@ export default function AuthRoutes(logger, router, securityContext) {
128
127
  const params = new URLSearchParams();
129
128
  params.append('code', /** @type {string} */ (code));
130
129
  params.append('state', /** @type {string} */ (state));
131
- params.append('client_id', process.env.GITHUB_CLIENT_ID);
132
- params.append('client_secret', process.env.GITHUB_CLIENT_SECRET);
130
+ params.append('client_id', /** @type {string} */ (process.env.GITHUB_CLIENT_ID));
131
+ params.append('client_secret', /** @type {string} */ (process.env.GITHUB_CLIENT_SECRET));
133
132
  params.append('redirect_uri', appUrl('/wuffle/login/callback'));
134
133
 
135
134
  const {
@@ -185,7 +184,9 @@ export default function AuthRoutes(logger, router, securityContext) {
185
184
  } = session;
186
185
 
187
186
  if (!githubUser) {
188
- return res.type('json').json(null) && null;
187
+ res.type('json').json(null);
188
+
189
+ return;
189
190
  }
190
191
 
191
192
  const {
@@ -216,17 +217,18 @@ export default function AuthRoutes(logger, router, securityContext) {
216
217
  }, 'failed to check GitHub authentication');
217
218
 
218
219
  // access is not granted anymore, clear current session
219
- return req.session.destroy(function(err) {
220
- return res.type('json').json(null);
221
- }) && null;
220
+ req.session.destroy(function(err) {
221
+ res.type('json').json(null);
222
+ });
223
+
224
+ return;
222
225
  }
223
226
  }
224
227
 
225
- return res.type('json').json({
228
+ res.type('json').json({
226
229
  login,
227
230
  avatar_url
228
- }) && null;
229
-
231
+ });
230
232
  });
231
233
 
232
234
 
@@ -31,14 +31,13 @@ export default function(webhookEvents, githubIssues, columns, issueFilter, logge
31
31
  'pull_request.closed'
32
32
  ], ifEnabled(async (context) => {
33
33
 
34
- const {
35
- pull_request,
36
- issue
37
- } = context.payload;
38
-
39
34
  const column = columns.getByState(DONE);
40
35
 
41
- await githubIssues.moveIssue(context, issue || pull_request, column);
36
+ const issueOrPull = 'issue' in context.payload
37
+ ? context.payload.issue
38
+ : context.payload.pull_request;
39
+
40
+ await githubIssues.moveIssue(context, issueOrPull, column);
42
41
  }));
43
42
 
44
43
  webhookEvents.on('pull_request.converted_to_draft', ifEnabled(async (context) => {
@@ -107,7 +106,7 @@ export default function(webhookEvents, githubIssues, columns, issueFilter, logge
107
106
  const newAssignee = (
108
107
  process.env.AUTO_ASSIGN_PULLS && !external &&
109
108
  author && author.type === 'User' && author.login
110
- );
109
+ ) || null;
111
110
 
112
111
  await Promise.all([
113
112
  githubIssues.moveIssue(context, pull_request, column, newAssignee),
@@ -172,7 +171,7 @@ export default function(webhookEvents, githubIssues, columns, issueFilter, logge
172
171
  return;
173
172
  }
174
173
 
175
- const issue_number = match[1];
174
+ const issue_number = parseInt(match[1], 10);
176
175
 
177
176
  const column = columns.getByState(IN_PROGRESS);
178
177
 
@@ -9,6 +9,17 @@ function isInternalError(error) {
9
9
  /**
10
10
  * This component performs a periodic background sync of a project.
11
11
  *
12
+ * Unless disabled via `process.env.DISABLE_BACKGROUND_SYNC` it will
13
+ * register a recurring check.
14
+ *
15
+ * Background check performs various optimizations to ensure only relevant
16
+ * data is stored on the board:
17
+ *
18
+ * * Closed issues/PRs on the board will be thrashed
19
+ * * Closed issues/PRs wiil not be synchronized from GitHub
20
+ * * Open but stale issues/PRs will not be synchronized to the board
21
+ * * Open issue/PR details will only be synchronized for recent issues
22
+ *
12
23
  * @constructor
13
24
  *
14
25
  * @param {Object} config
@@ -21,25 +32,25 @@ export default function BackgroundSync(config, logger, store, events, background
21
32
 
22
33
  // 30 days
23
34
  const syncClosedLookback = (
24
- parseInt(process.env.BACKGROUND_SYNC_SYNC_CLOSED_LOOKBACK, 10) ||
35
+ parseInt(process.env.BACKGROUND_SYNC_SYNC_CLOSED_LOOKBACK || '', 10) ||
25
36
  1000 * 60 * 60 * 24 * 30
26
37
  );
27
38
 
28
39
  // 4 hours
29
40
  const syncClosedDetailsLookback = (
30
- parseInt(process.env.BACKGROUND_SYNC_SYNC_CLOSED_DETAILS_LOOKBACK, 10) ||
41
+ parseInt(process.env.BACKGROUND_SYNC_SYNC_CLOSED_DETAILS_LOOKBACK || '', 10) ||
31
42
  1000 * 60 * 60 * 4
32
43
  );
33
44
 
34
45
  // 1 day
35
46
  const syncOpenDetailsLookback = (
36
- parseInt(process.env.BACKGROUND_SYNC_SYNC_OPEN_DETAILS_LOOKBACK, 10) ||
47
+ parseInt(process.env.BACKGROUND_SYNC_SYNC_OPEN_DETAILS_LOOKBACK || '', 10) ||
37
48
  1000 * 60 * 60 * 24
38
49
  );
39
50
 
40
51
  // 60 days
41
52
  const removeClosedLookback = (
42
- parseInt(process.env.BACKGROUND_SYNC_REMOVE_CLOSED_LOOKBACK, 10) ||
53
+ parseInt(process.env.BACKGROUND_SYNC_REMOVE_CLOSED_LOOKBACK || '', 10) ||
43
54
  1000 * 60 * 60 * 24 * 60
44
55
  );
45
56
 
@@ -393,6 +404,13 @@ We automatically synchronize all repositories you granted us access to via the G
393
404
  return Promise.all(jobs);
394
405
  }
395
406
 
407
+ /**
408
+ * Trigger background synchronization for all connected repositories.
409
+ *
410
+ * This ensures that data out-of-sync with the board is fetched from remote.
411
+ *
412
+ * @return {Promise<void>}
413
+ */
396
414
  async function backgroundSync() {
397
415
 
398
416
  log.info('start');
@@ -410,7 +428,7 @@ We automatically synchronize all repositories you granted us access to via the G
410
428
  }
411
429
 
412
430
  const syncInterval = (
413
- parseInt(process.env.BACKGROUND_SYNC_SYNC_INTERVAL, 10) || (
431
+ parseInt(process.env.BACKGROUND_SYNC_SYNC_INTERVAL || '', 10) || (
414
432
  process.env.NODE_ENV !== 'production'
415
433
 
416
434
  // five minutes
@@ -442,6 +460,13 @@ We automatically synchronize all repositories you granted us access to via the G
442
460
 
443
461
  // api ///////////////////
444
462
 
463
+ /**
464
+ * Trigger background synchronization for all connected repositories.
465
+ *
466
+ * This ensures that data out-of-sync with the board is fetched from remote.
467
+ *
468
+ * @return {Promise<void>}
469
+ */
445
470
  this.backgroundSync = backgroundSync;
446
471
 
447
472
 
@@ -172,7 +172,7 @@ export default async function BoardApiRoutes(
172
172
  const items = store.getBoard();
173
173
  const cursor = store.getUpdateCursor();
174
174
 
175
- return filterBoardItems(req, items).then(filteredItems => {
175
+ filterBoardItems(req, items).then(filteredItems => {
176
176
 
177
177
  return res.type('json').json({
178
178
  items: filteredItems,
@@ -196,7 +196,7 @@ export default async function BoardApiRoutes(
196
196
  name
197
197
  } = config;
198
198
 
199
- return res.type('json').json({
199
+ res.type('json').json({
200
200
  columns: columns.map(c => {
201
201
  const { name, collapsed } = c;
202
202
 
@@ -236,7 +236,9 @@ export default async function BoardApiRoutes(
236
236
  const user = authRoutes.getGitHubUser(req);
237
237
 
238
238
  if (!user) {
239
- return res.status(401).json({}) && null;
239
+ res.status(401).json({});
240
+
241
+ return;
240
242
  }
241
243
 
242
244
  const body = JSON.parse(req.body);
@@ -251,13 +253,17 @@ export default async function BoardApiRoutes(
251
253
  const issue = await store.getIssueById(id);
252
254
 
253
255
  if (!issue) {
254
- return res.status(404).json({}) && null;
256
+ res.status(404).json({});
257
+
258
+ return;
255
259
  }
256
260
 
257
261
  const column = columns.getByName(columnName);
258
262
 
259
263
  if (!column) {
260
- return res.status(404).json({}) && null;
264
+ res.status(404).json({});
265
+
266
+ return;
261
267
  }
262
268
 
263
269
  const repo = repoAndOwner(issue);
@@ -265,7 +271,9 @@ export default async function BoardApiRoutes(
265
271
  const canWrite = await userAccess.canWrite(user, repo);
266
272
 
267
273
  if (!canWrite) {
268
- return res.status(403).json({}) && null;
274
+ res.status(403).json({});
275
+
276
+ return;
269
277
  }
270
278
 
271
279
  const octokit = await githubClient.getUserScoped(user);
@@ -280,15 +288,13 @@ export default async function BoardApiRoutes(
280
288
  }
281
289
  };
282
290
 
283
- return (
284
- moveIssue(context, issue, column, { before, after }).then(() => {
285
- res.type('json').json({});
286
- }).catch(err => {
287
- log.error(err, 'failed to move issue');
291
+ moveIssue(context, issue, column, { before, after }).then(() => {
292
+ res.type('json').json({});
293
+ }).catch(err => {
294
+ log.error(err, 'failed to move issue');
288
295
 
289
- res.status(500).json({ error : true });
290
- })
291
- ) && null;
296
+ res.status(500).json({ error : true });
297
+ });
292
298
 
293
299
  });
294
300
 
@@ -16,6 +16,18 @@ export default function S3() {
16
16
  S3_ENDPOINT: endpoint
17
17
  } = process.env;
18
18
 
19
+ if (!accessKeyId) {
20
+ throw new Error('process.env.AWS_ACCESS_KEY_ID required');
21
+ }
22
+
23
+ if (!secretAccessKey) {
24
+ throw new Error('process.env.AWS_SECRET_ACCESS_KEY required');
25
+ }
26
+
27
+ if (!bucket) {
28
+ throw new Error('process.env.S3_BUCKET required');
29
+ }
30
+
19
31
  const s3client = new S3Client({
20
32
  region,
21
33
  endpoint,
@@ -312,7 +312,7 @@ export default function GithubApp(config, app, logger, injector) {
312
312
  /**
313
313
  * Get an installation for the given id.
314
314
  *
315
- * @param {string} id
315
+ * @param {string|number} id
316
316
  *
317
317
  * @return {Promise<Installation | undefined>}
318
318
  */
@@ -3,6 +3,8 @@ import { repoAndOwner, Cache } from '../../util/index.js';
3
3
  /**
4
4
  * This component updates the stored issues based on GitHub events.
5
5
  *
6
+ * It also hooks into `BackgroundSync` to fetch checks for a PR.
7
+ *
6
8
  * @constructor
7
9
  *
8
10
  * @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
@@ -1,21 +1,21 @@
1
- import { repoAndOwner } from '../../util/index.js';
2
1
  import { filterUser, filterIssue } from '../../filters.js';
3
- import gql from 'fake-tag';
4
2
 
5
3
 
6
4
  /**
7
5
  * This component updates the stored issues based on GitHub events.
8
6
  *
7
+ * It also hooks into `BackgroundSync` to fetch comments for issues/PRs.
8
+ *
9
9
  * @constructor
10
10
  *
11
11
  * @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
12
12
  * @param {import('../../events.js').default} events
13
- * @param {import('../github-client/GithubClient.js').default} githubClient
14
13
  * @param {import('../../store.js').default} store
15
14
  * @param {import('../issue-filter/IssueFilter.js').default} issueFilter
16
15
  * @param {import('../../types.js').Logger } logger
16
+ * @param {import('./GithubCommentsBackend.js').default} githubCommentsBackend
17
17
  */
18
- export default function GithubComments(webhookEvents, events, githubClient, store, issueFilter, logger) {
18
+ export default function GithubComments(webhookEvents, events, store, issueFilter, logger, githubCommentsBackend) {
19
19
 
20
20
  const log = logger.child({
21
21
  name: 'wuffle:github-comments'
@@ -32,81 +32,10 @@ export default function GithubComments(webhookEvents, events, githubClient, stor
32
32
  } = event;
33
33
 
34
34
  const {
35
- id,
36
- number
35
+ id
37
36
  } = issue;
38
37
 
39
- const {
40
- repo,
41
- owner
42
- } = repoAndOwner(issue);
43
-
44
- const github = await githubClient.getOrgScoped(owner);
45
-
46
- const result = await github.graphql(gql`
47
-
48
- fragment CommentInfo on IssueComment {
49
- id: databaseId
50
- node_id: id
51
- body: bodyText
52
- created_at: publishedAt
53
- authorAssociation,
54
- html_url: url,
55
- user: author {
56
- login
57
- avatar_url: avatarUrl,
58
- html_url: url
59
- }
60
- }
61
-
62
- query FetchComments(
63
- $repo: String!,
64
- $owner: String!,
65
- $issue_number: Int!,
66
- $after: String
67
- ) {
68
- repository(name: $repo, owner: $owner) {
69
- issueOrPullRequest(number: $issue_number) {
70
- ...on Issue {
71
- comments(first: 100, after: $after) {
72
- edges {
73
- node {
74
- ...CommentInfo
75
- }
76
- }
77
- pageInfo {
78
- endCursor
79
- hasNextPage
80
- }
81
- totalCount
82
- }
83
- }
84
- ...on PullRequest {
85
- comments(first: 100, after: $after) {
86
- edges {
87
- node {
88
- ...CommentInfo
89
- }
90
- }
91
- pageInfo {
92
- endCursor
93
- hasNextPage
94
- }
95
- totalCount
96
- }
97
- }
98
- }
99
- }
100
- }`,
101
- {
102
- owner,
103
- repo,
104
- issue_number: number
105
- });
106
-
107
- const comments = (
108
- result.repository.issueOrPullRequest.comments.edges.map(e => e.node)
109
- );
38
+ const comments = await githubCommentsBackend.getIssueComments(issue);
110
39
 
111
40
  await store.queueUpdate({
112
41
  id,
@@ -115,7 +44,9 @@ export default function GithubComments(webhookEvents, events, githubClient, stor
115
44
  });
116
45
 
117
46
  webhookEvents.on([
118
- 'issue_comment'
47
+ 'issue_comment.created',
48
+ 'issue_comment.edited',
49
+ 'issue_comment.deleted'
119
50
  ], ifEnabled(async ({ payload }) => {
120
51
  const {
121
52
  action,
@@ -0,0 +1,132 @@
1
+ import { repoAndOwner } from '../../util/index.js';
2
+
3
+ import gql from 'fake-tag';
4
+
5
+ /**
6
+ * @typedef { {
7
+ * id: number,
8
+ * node_id: string,
9
+ * body: string,
10
+ * created_at: string,
11
+ * authorAssociation: string,
12
+ * html_url: string,
13
+ * user: {
14
+ * id: number,
15
+ * node_id: string,
16
+ * login: string,
17
+ * avatar_url: string,
18
+ * html_url: string,
19
+ * type: string
20
+ * }
21
+ * } } GithubComment
22
+ *
23
+ * @typedef { import('../../util/meta.js').Issue } Issue
24
+ */
25
+
26
+
27
+ /**
28
+ * This component fetches GitHub comments data required for background synchronization.
29
+ *
30
+ * @constructor
31
+ *
32
+ * @param {import('../github-client/GithubClient.js').default} githubClient
33
+ */
34
+ export default function GithubCommentsBackend(githubClient) {
35
+
36
+ /**
37
+ * Return comments for given issue.
38
+ *
39
+ * @param {Issue} issue
40
+ *
41
+ * @return {Promise<GithubComment[]>}
42
+ */
43
+ this.getIssueComments = async function(issue) {
44
+
45
+ const {
46
+ number
47
+ } = issue;
48
+
49
+ const {
50
+ repo,
51
+ owner
52
+ } = repoAndOwner(issue);
53
+
54
+ const github = await githubClient.getOrgScoped(owner);
55
+
56
+ const result = await github.graphql(gql`
57
+
58
+ fragment CommentInfo on IssueComment {
59
+ id: databaseId
60
+ node_id: id
61
+ body: bodyText
62
+ created_at: publishedAt
63
+ authorAssociation,
64
+ html_url: url,
65
+ user: author {
66
+ login
67
+ avatar_url: avatarUrl,
68
+ html_url: url,
69
+ type: __typename
70
+ ... on User {
71
+ id: databaseId
72
+ node_id: id
73
+ }
74
+ ... on Bot {
75
+ id: databaseId
76
+ node_id: id
77
+ }
78
+ }
79
+ }
80
+
81
+ query FetchComments(
82
+ $repo: String!,
83
+ $owner: String!,
84
+ $issue_number: Int!,
85
+ $after: String
86
+ ) {
87
+ repository(name: $repo, owner: $owner) {
88
+ issueOrPullRequest(number: $issue_number) {
89
+ ...on Issue {
90
+ comments(first: 100, after: $after) {
91
+ edges {
92
+ node {
93
+ ...CommentInfo
94
+ }
95
+ }
96
+ pageInfo {
97
+ endCursor
98
+ hasNextPage
99
+ }
100
+ totalCount
101
+ }
102
+ }
103
+ ...on PullRequest {
104
+ comments(first: 100, after: $after) {
105
+ edges {
106
+ node {
107
+ ...CommentInfo
108
+ }
109
+ }
110
+ pageInfo {
111
+ endCursor
112
+ hasNextPage
113
+ }
114
+ totalCount
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }`,
120
+ {
121
+ owner,
122
+ repo,
123
+ issue_number: number
124
+ });
125
+
126
+ const comments = /** @type { GithubComment[] } */ (
127
+ result.repository.issueOrPullRequest.comments.edges.map(e => e.node)
128
+ );
129
+
130
+ return comments;
131
+ };
132
+ }
@@ -1,6 +1,8 @@
1
1
  import GithubComments from './GithubComments.js';
2
+ import GithubCommentsBackend from './GithubCommentsBackend.js';
2
3
 
3
4
  export default {
4
5
  __init__: [ 'githubComments' ],
5
- githubComments: [ 'type', GithubComments ]
6
+ githubComments: [ 'type', GithubComments ],
7
+ githubCommentsBackend: [ 'type', GithubCommentsBackend ]
6
8
  };
@@ -4,8 +4,17 @@ const {
4
4
  CLOSES
5
5
  } = linkTypes;
6
6
 
7
+ /**
8
+ * @typedef { import('probot').Context } ProbotContext
9
+ *
10
+ * @typedef { import('../../columns.js').ColumnDefinition } ColumnDefinition
11
+ */
7
12
 
8
13
  /**
14
+ * Moves GitHub issues between board columns by applying the
15
+ * corresponding state (open/closed), label, and assignee changes
16
+ * via the GitHub API.
17
+ *
9
18
  * @constructor
10
19
  *
11
20
  * @param {any} config
@@ -18,6 +27,16 @@ export default function GithubIssues(config, logger, columns) {
18
27
  name: 'wuffle:github-issues'
19
28
  });
20
29
 
30
+ /**
31
+ * Return the assignee update required to add `newAssignee` to the
32
+ * issue, or an empty object if the assignee is already set or not
33
+ * provided.
34
+ *
35
+ * @param {Object} issue
36
+ * @param {string|null} newAssignee
37
+ *
38
+ * @return {{ assignees?: string[] }}
39
+ */
21
40
  function getAssigneeUpdate(issue, newAssignee) {
22
41
 
23
42
  if (!newAssignee) {
@@ -39,6 +58,15 @@ export default function GithubIssues(config, logger, columns) {
39
58
 
40
59
  }
41
60
 
61
+ /**
62
+ * Return the state update (`open` / `closed`) required to reflect
63
+ * the new column, or an empty object if no change is needed.
64
+ *
65
+ * @param {Object} issue
66
+ * @param {ColumnDefinition} newColumn
67
+ *
68
+ * @return {{ state?: 'open'|'closed' }}
69
+ */
42
70
  function getStateUpdate(issue, newColumn) {
43
71
 
44
72
  let update = {};
@@ -55,16 +83,29 @@ export default function GithubIssues(config, logger, columns) {
55
83
  return update;
56
84
  }
57
85
 
86
+ /**
87
+ * Return the label changes required to reflect the new column:
88
+ * the column label to add and any other column labels to remove.
89
+ *
90
+ * @param {Object} issue
91
+ * @param {ColumnDefinition} newColumn
92
+ *
93
+ * @return {{ addLabels: string[], removeLabels: string[] }}
94
+ */
58
95
  function getLabelUpdate(issue, newColumn) {
59
96
 
60
- const issueLabels = issue.labels.map(l => l.name);
97
+ const issueLabels = /** @type { string[] } */ (
98
+ issue.labels.map(l => l.name)
99
+ );
61
100
 
62
101
  const newLabel = newColumn.label;
63
102
 
64
103
  const addLabels = (!newLabel || issueLabels.includes(newLabel)) ? [] : [ newLabel ];
65
104
 
66
- const removeLabels = columns.getAll().map(c => c.label).filter(
67
- label => label && label !== newLabel && issueLabels.includes(label)
105
+ const removeLabels = /** @type { string[] } */ (
106
+ columns.getAll().map(c => c.label).filter(
107
+ label => label && label !== newLabel && issueLabels.includes(label)
108
+ )
68
109
  );
69
110
 
70
111
  return {
@@ -73,6 +114,10 @@ export default function GithubIssues(config, logger, columns) {
73
114
  };
74
115
  }
75
116
 
117
+ /**
118
+ * @param {ProbotContext} context
119
+ * @param {number} issue_number
120
+ */
76
121
  function findIssue(context, issue_number) {
77
122
 
78
123
  const params = context.repo({ issue_number });
@@ -88,12 +133,43 @@ export default function GithubIssues(config, logger, columns) {
88
133
  });
89
134
  }
90
135
 
91
- function findAndMoveIssue(context, number, newColumn, newAssignee, test = (issue) => true) {
92
- return findIssue(context, number)
93
- .then((issue) => issue && test(issue) && moveIssue(context, issue, newColumn, newAssignee));
136
+ /**
137
+ * Fetch an issue by number and move it to a new column if the
138
+ * optional predicate passes. Resolves to `false` if the issue is
139
+ * not found or the predicate rejects it.
140
+ *
141
+ * @param {ProbotContext} context
142
+ * @param {number} number
143
+ * @param {ColumnDefinition} newColumn
144
+ * @param {string|null} newAssignee
145
+ * @param {(issue: Object) => boolean} [test]
146
+ *
147
+ * @return {Promise<void|false>}
148
+ */
149
+ async function findAndMoveIssue(context, number, newColumn, newAssignee, test = (issue) => true) {
150
+ const issue = await findIssue(context, number);
151
+
152
+ if (!issue || !test(issue)) {
153
+ return false;
154
+ }
155
+
156
+ return moveIssue(context, issue, newColumn, newAssignee);
94
157
  }
95
158
 
96
- async function moveReferencedIssues(context, issue, newColumn, newAssignee) {
159
+ /**
160
+ * Move all issues referenced via `closes` links in the given issue
161
+ * or pull request body to a new column.
162
+ *
163
+ * Only issues within the same repository are moved.
164
+ *
165
+ * @param {ProbotContext} context
166
+ * @param {Object} issue
167
+ * @param {ColumnDefinition} newColumn
168
+ * @param {string|null} [newAssignee=null]
169
+ *
170
+ * @return {Promise<(void|false)[]>}
171
+ */
172
+ async function moveReferencedIssues(context, issue, newColumn, newAssignee = null) {
97
173
 
98
174
  // TODO(nikku): do that lazily, i.e. react to PR label changes?
99
175
  // would slower the movement but support manual moving-issue with PR
@@ -128,7 +204,18 @@ export default function GithubIssues(config, logger, columns) {
128
204
  }));
129
205
  }
130
206
 
131
- function moveIssue(context, issue, newColumn, newAssignee) {
207
+ /**
208
+ * Move an issue to a new column, applying state, label, and
209
+ * assignee updates via the GitHub API.
210
+ *
211
+ * @param {ProbotContext} context
212
+ * @param {Object} issue
213
+ * @param {ColumnDefinition} newColumn
214
+ * @param {string|null} [newAssignee=null]
215
+ *
216
+ * @return {Promise<void>}
217
+ */
218
+ function moveIssue(context, issue, newColumn, newAssignee = null) {
132
219
 
133
220
  const {
134
221
  number: issue_number
@@ -199,22 +286,87 @@ export default function GithubIssues(config, logger, columns) {
199
286
  );
200
287
  }
201
288
 
202
- return Promise.all(invocations);
289
+ return Promise.all(invocations).then(() => {});
203
290
  }
204
291
 
205
292
 
206
293
  // api /////////////////////////////
207
294
 
295
+ /**
296
+ * Move an issue to a new column, applying state, label, and
297
+ * assignee updates via the GitHub API.
298
+ *
299
+ * @param {ProbotContext} context
300
+ * @param {Object} issue
301
+ * @param {ColumnDefinition} newColumn
302
+ * @param {string} [newAssignee]
303
+ *
304
+ * @return {Promise<void>}
305
+ */
208
306
  this.moveIssue = moveIssue;
209
307
 
308
+ /**
309
+ * Move all issues referenced via `closes` links in the given issue
310
+ * or pull request body to a new column.
311
+ *
312
+ * Only issues within the same repository are moved.
313
+ *
314
+ * @param {ProbotContext} context
315
+ * @param {Object} issue
316
+ * @param {ColumnDefinition} newColumn
317
+ * @param {string|null} [newAssignee=null]
318
+ *
319
+ * @return {Promise<(void|false)[]>}
320
+ */
210
321
  this.moveReferencedIssues = moveReferencedIssues;
211
322
 
323
+ /**
324
+ * Return the state update (`open` / `closed`) required to reflect
325
+ * the new column, or an empty object if no change is needed.
326
+ *
327
+ * @param {Object} issue
328
+ * @param {ColumnDefinition} newColumn
329
+ *
330
+ * @return {{ state?: 'open'|'closed' }}
331
+ */
212
332
  this.getStateUpdate = getStateUpdate;
213
333
 
334
+ /**
335
+ * Return the assignee update required to add `newAssignee` to the
336
+ * issue, or an empty object if the assignee is already set or not
337
+ * provided.
338
+ *
339
+ * @param {Object} issue
340
+ * @param {string} [newAssignee]
341
+ *
342
+ * @return {{ assignees?: string[] }}
343
+ */
214
344
  this.getAssigneeUpdate = getAssigneeUpdate;
215
345
 
346
+ /**
347
+ * Return the label changes required to reflect the new column:
348
+ * the column label to add and any other column labels to remove.
349
+ *
350
+ * @param {Object} issue
351
+ * @param {ColumnDefinition} newColumn
352
+ *
353
+ * @return {{ addLabels: string[], removeLabels: string[] }}
354
+ */
216
355
  this.getLabelUpdate = getLabelUpdate;
217
356
 
357
+ /**
358
+ * Fetch an issue by number and move it to a new column if the
359
+ * optional predicate passes. Resolves to `false` if the issue is
360
+ * not found or the predicate rejects it.
361
+ *
362
+ * @param {ProbotContext} context
363
+ * @param {number} number
364
+ * @param {ColumnDefinition} newColumn
365
+ * @param {string|null} newAssignee
366
+ * @param {(issue: Object) => boolean} [test]
367
+ *
368
+ * @return {Promise<void|false>}
369
+ */
218
370
  this.findAndMoveIssue = findAndMoveIssue;
219
371
 
220
372
  }
@@ -5,6 +5,8 @@ import { filterUser, filterPull } from '../../filters.js';
5
5
  /**
6
6
  * This component updates the stored issues based on GitHub events.
7
7
  *
8
+ * It also hooks into `BackgroundSync` to fetch reviews for a PR.
9
+ *
8
10
  * @constructor
9
11
  *
10
12
  * @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
@@ -5,6 +5,8 @@ import { filterPull } from '../../filters.js';
5
5
  /**
6
6
  * This component updates synchronizes GitHub statuses with pull requests.
7
7
  *
8
+ * It also hooks into `BackgroundSync` to fetch statuses for a PR.
9
+ *
8
10
  * @constructor
9
11
  *
10
12
  * @param {import('../webhook-events/WebhookEvents.js').default} webhookEvents
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * @typedef { { ignoreFilter?: string } } StoreFilterConfig
3
+ *
4
+ * @typedef { Extract<import('@octokit/webhooks').EmitterWebhookEventName, 'issues' | `issues.${string}` | 'issue_comment' | `issue_comment.${string}` | 'pull_request' | `pull_request.${string}` | 'pull_request_review' | `pull_request_review.${string}` | 'pull_request_review_comment' | `pull_request_review_comment.${string}` | 'pull_request_review_thread' | `pull_request_review_thread.${string}`> } IssueOrPullWebhookEventName
3
5
  */
4
6
 
5
7
  import { filterIssueOrPull } from '../../filters.js';
@@ -41,7 +43,7 @@ export default function IssueFilter(config, store, search, logger) {
41
43
  /**
42
44
  * @param {import('../../types.js').Logger } log
43
45
  *
44
- * @return { (filterFn) => (any) => any }
46
+ * @return {<E extends IssueOrPullWebhookEventName>(fn: (context: import('probot').Context<E>) => any) => (context: import('probot').Context<E>) => any}
45
47
  */
46
48
  function createWebhookFilter(log) {
47
49
 
@@ -49,7 +51,7 @@ export default function IssueFilter(config, store, search, logger) {
49
51
 
50
52
  return (context) => {
51
53
 
52
- const payload = context.payload;
54
+ const payload = /** @type {any} */ (context.payload);
53
55
 
54
56
  const issueOrPull = filterIssueOrPull(
55
57
  payload.issue || payload.pull_request,
@@ -79,7 +81,7 @@ export default function IssueFilter(config, store, search, logger) {
79
81
  /**
80
82
  * @param {import('../../types.js').Logger } log
81
83
  *
82
- * @return { (filterFn) => (any) => any }
84
+ * @return {<E extends IssueOrPullWebhookEventName>(fn: (context: import('probot').Context<E>) => any) => (context: import('probot').Context<E>) => any}
83
85
  */
84
86
  this.createWebhookFilter = createWebhookFilter;
85
87
  }
@@ -37,7 +37,7 @@ export default function Search(config, logger, store) {
37
37
 
38
38
  const {
39
39
  treatBotsAsReviewers = false,
40
- defaultFilter
40
+ defaultFilter = ''
41
41
  } = config;
42
42
 
43
43
  function filterNoop(issue) {
@@ -63,13 +63,17 @@ export default function Search(config, logger, store) {
63
63
  *
64
64
  * @return { boolean }
65
65
  */
66
- function includes(actual, pattern, exact) {
66
+ function includes(actual, pattern, exact = false) {
67
+
68
+ if (!pattern) {
69
+ return false;
70
+ }
67
71
 
68
72
  if (exact) {
69
- return pattern && actual === pattern;
73
+ return actual === pattern;
70
74
  }
71
75
 
72
- return pattern && actual.toLowerCase().includes(pattern.toLowerCase());
76
+ return actual.toLowerCase().includes(pattern.toLowerCase());
73
77
  }
74
78
 
75
79
  function isPull(issue) {
@@ -445,7 +449,7 @@ export default function Search(config, logger, store) {
445
449
  * Retrieve a filter function from the given search string.
446
450
  *
447
451
  * @param {string} search
448
- * @param {GitHubUser} [user]
452
+ * @param {GitHubUser|null} [user]
449
453
  *
450
454
  * @return {FilterFn}
451
455
  */
@@ -1,4 +1,11 @@
1
1
  /**
2
+ * @typedef { import('probot').Probot } Probot
3
+ * @typedef { import('@octokit/webhooks').EmitterWebhookEventName } WebhookEventName
4
+ */
5
+
6
+ /**
7
+ * A lightweight wrapper around {@link Probot#on} and {@link Probot#onAny}.
8
+ *
2
9
  * @constructor
3
10
  *
4
11
  * @param {import('../../types.js').ProbotApp} app
@@ -7,8 +14,7 @@
7
14
  export default function WebhookEvents(app, githubApp) {
8
15
 
9
16
  /**
10
- * @template {Function} T
11
- * @param {T} fn
17
+ * @param {(context: import('probot').Context) => any} fn
12
18
  */
13
19
  function ifEnabled(fn) {
14
20
 
@@ -37,8 +43,9 @@ export default function WebhookEvents(app, githubApp) {
37
43
  * Register a event lister for a single
38
44
  * or a number of webhook events.
39
45
  *
40
- * @param {any|any[]} events
41
- * @param {Function} fn listener
46
+ * @template {WebhookEventName} E
47
+ * @param {E | E[]} events
48
+ * @param {(context: import('probot').Context<E>) => any} fn listener
42
49
  */
43
50
  function on(events, fn) {
44
51
  app.on(events, ifEnabled(fn));
@@ -48,7 +55,7 @@ export default function WebhookEvents(app, githubApp) {
48
55
  * Register an event listener for all
49
56
  * webhook events.
50
57
  *
51
- * @param {Function} fn
58
+ * @param {(context: import('probot').Context) => any} fn
52
59
  */
53
60
  function onAny(fn) {
54
61
  app.onAny(ifEnabled(fn));
package/lib/events.js CHANGED
@@ -139,8 +139,8 @@ Events.prototype.createEvent = function(data) {
139
139
  * @param {Object} [data] the event object
140
140
  * @param {...Object} additionalArgs to be passed to the callback functions
141
141
  *
142
- * @return {Promise<boolean>} the events return value, if specified or false if the
143
- * default action was prevented by listeners
142
+ * @return {Promise<boolean|undefined|any>} the events return value, if
143
+ * specified or false if the default action was prevented by listeners
144
144
  */
145
145
  Events.prototype.emit = async function(type, data, ...additionalArgs) {
146
146
 
@@ -103,7 +103,8 @@ function validateSetup() {
103
103
 
104
104
  const setup = new ManifestCreation();
105
105
 
106
- const manifest = JSON.parse(setup.getManifest(setup.pkg, process.env.BASE_URL));
106
+ // TODO(nikku): process.env.BASE_URL may not be defined here
107
+ const manifest = JSON.parse(setup.getManifest(setup.pkg, /** @type { string } */ (process.env.BASE_URL)));
107
108
 
108
109
  return [
109
110
  !manifest.url && new Error('No <url> configured in app.yml'),
@@ -116,7 +116,7 @@ export function parseSearch(str) {
116
116
 
117
117
  do {
118
118
  lastTerm = stack.pop();
119
- } while (!lastTerm.group);
119
+ } while (lastTerm && !lastTerm.group);
120
120
 
121
121
  continue;
122
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuffle",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "A multi-repository task board for GitHub issues",
5
5
  "author": {
6
6
  "name": "Nico Rehwaldt",
@@ -63,7 +63,7 @@
63
63
  "@types/mocha": "^10.0.10",
64
64
  "@types/sinon": "^21.0.1",
65
65
  "chai": "^6.2.2",
66
- "graphql": "^16.13.2",
66
+ "graphql": "^16.14.0",
67
67
  "mocha": "^11.7.5",
68
68
  "nock": "^14.0.13",
69
69
  "nodemon": "^3.1.14",
@@ -83,5 +83,5 @@
83
83
  "index.js",
84
84
  "wuffle.config.example.js"
85
85
  ],
86
- "gitHead": "ea9ac74526b02f9834b5e926df0e8e82536eba07"
86
+ "gitHead": "132f68f8372f71a15c4e34b0fd9db837ccc02b6b"
87
87
  }