buddy-workbench 0.2.9 → 0.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -354,24 +354,49 @@ async function searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates
354
354
  }
355
355
 
356
356
  async function fetchCommentsForIssue(jiraHost, headers, issueKey, maxResults = 50) {
357
- try {
358
- const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
359
- const response = await httpClient.get(url, { headers });
360
- if (response.status !== 200) return [];
361
- const total = response.data?.total || 0;
362
- const comments = Array.isArray(response.data?.comments) ? response.data.comments : [];
363
- if (total > comments.length && total > maxResults) {
364
- const startAt = Math.max(0, total - maxResults);
365
- const latestUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?startAt=${startAt}&maxResults=${maxResults}`;
366
- const latestResponse = await httpClient.get(latestUrl, { headers });
367
- if (latestResponse.status === 200 && Array.isArray(latestResponse.data?.comments)) {
368
- return latestResponse.data.comments;
357
+ const requestComments = async (url) => {
358
+ const maxRetries = 4;
359
+ for (let attempt = 0; ; attempt += 1) {
360
+ const response = await httpClient.get(url, { headers });
361
+ if (response.status === 200 && Array.isArray(response.data?.comments)) return response;
362
+ if (response.status !== 429 || attempt >= maxRetries) {
363
+ const error = new Error(`Jira comments request for ${issueKey} returned HTTP ${response.status}`);
364
+ error.status = response.status;
365
+ throw error;
369
366
  }
367
+
368
+ const retryAfter = response.headers?.['retry-after'] ?? response.headers?.['Retry-After'];
369
+ const retryAfterSeconds = Number(retryAfter);
370
+ const retryAfterDate = Date.parse(retryAfter);
371
+ const delayMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0
372
+ ? retryAfterSeconds * 1000
373
+ : Number.isFinite(retryAfterDate)
374
+ ? Math.max(0, retryAfterDate - Date.now())
375
+ : Math.min(1000 * (2 ** attempt), 10000);
376
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
370
377
  }
371
- return comments;
372
- } catch {
373
- return [];
378
+ };
379
+
380
+ const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
381
+ const response = await requestComments(url);
382
+ const total = response.data?.total || 0;
383
+ const comments = response.data.comments;
384
+ if (total > comments.length && total > maxResults) {
385
+ const startAt = Math.max(0, total - maxResults);
386
+ const latestUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?startAt=${startAt}&maxResults=${maxResults}`;
387
+ const latestResponse = await requestComments(latestUrl);
388
+ return latestResponse.data.comments;
389
+ }
390
+ return comments;
391
+ }
392
+
393
+ async function mapInBatches(items, batchSize, mapper) {
394
+ const results = [];
395
+ for (let index = 0; index < items.length; index += batchSize) {
396
+ const batchResults = await Promise.all(items.slice(index, index + batchSize).map(mapper));
397
+ results.push(...batchResults);
374
398
  }
399
+ return results;
375
400
  }
376
401
 
377
402
  router.get('/recently-created', (_req, res) => {
@@ -409,7 +434,7 @@ router.get('/recently-commented', async (_req, res) => {
409
434
  const issues = await searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter');
410
435
  const results = [];
411
436
 
412
- const issueCommentsPromises = issues.map(async (issue) => {
437
+ const issueComments = await mapInBatches(issues, 5, async (issue) => {
413
438
  const key = issue.key;
414
439
  const fields = issue.fields || {};
415
440
  const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
@@ -436,8 +461,7 @@ router.get('/recently-commented', async (_req, res) => {
436
461
  return null;
437
462
  });
438
463
 
439
- const settled = await Promise.all(issueCommentsPromises);
440
- for (const item of settled) {
464
+ for (const item of issueComments) {
441
465
  if (item) results.push(item);
442
466
  }
443
467
 
@@ -478,7 +502,7 @@ router.get('/recently-mentioned', async (_req, res) => {
478
502
  const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter', 40);
479
503
  const results = [];
480
504
 
481
- const issueMentionsPromises = issues.slice(0, 30).map(async (issue) => {
505
+ const issueMentions = await mapInBatches(issues.slice(0, 30), 5, async (issue) => {
482
506
  const key = issue.key;
483
507
  const fields = issue.fields || {};
484
508
  const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
@@ -511,8 +535,7 @@ router.get('/recently-mentioned', async (_req, res) => {
511
535
  return null;
512
536
  });
513
537
 
514
- const settled = await Promise.all(issueMentionsPromises);
515
- for (const item of settled) {
538
+ for (const item of issueMentions) {
516
539
  if (item) results.push(item);
517
540
  }
518
541
 
@@ -12,6 +12,7 @@ import { recordApiError } from '../lib/api-errors.js';
12
12
  const router = Router();
13
13
  const exec = promisify(execFile);
14
14
  const running = new Set();
15
+ const BITBUCKET_REQUEST_BATCH_SIZE = 5;
15
16
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
16
17
  const httpClient = axios.create({
17
18
  httpsAgent,
@@ -239,14 +240,24 @@ router.post('/repos/branches', async (req, res) => {
239
240
  const repos = repoIds.map((repoId) => data.repos.find((repo) => repo.id === repoId)).filter(Boolean);
240
241
  if (!repos.length) return res.json([]);
241
242
  try {
242
- const remote = await Promise.all(repos.map((repo) => fetchRemoteBranches(repo)));
243
+ const remote = [];
244
+ for (let index = 0; index < repos.length; index += BITBUCKET_REQUEST_BATCH_SIZE) {
245
+ remote.push(...await Promise.all(repos.slice(index, index + BITBUCKET_REQUEST_BATCH_SIZE).map(fetchRemoteBranches)));
246
+ }
243
247
  const branchSets = remote.map(({ branches }) => new Set(branches.map((branch) => branch.name)));
244
248
  const commonNames = remote[0].branches.map((branch) => branch.name).filter((name) => branchSets.every((branches) => branches.has(name)));
245
- const commonBranches = await Promise.all(commonNames.map(async (name) => {
246
- const branches = remote.map(({ base, branches: items }) => ({ base, branch: items.find((item) => item.name === name) })).filter(({ branch }) => branch);
247
- const updated = await Promise.all(branches.map(({ base, branch }) => fetchBranchUpdatedAt(base, branch)));
248
- return { name, updatedAt: Math.max(...updated.map((item) => item.updatedAt)), rank: Math.min(...updated.map((item) => item.rank)) };
249
- }));
249
+ const commonBranches = [];
250
+ for (let index = 0; index < commonNames.length; index += BITBUCKET_REQUEST_BATCH_SIZE) {
251
+ const batch = await Promise.all(commonNames.slice(index, index + BITBUCKET_REQUEST_BATCH_SIZE).map(async (name) => {
252
+ const branches = remote.map(({ base, branches: items }) => ({ base, branch: items.find((item) => item.name === name) })).filter(({ branch }) => branch);
253
+ const updated = [];
254
+ for (let branchIndex = 0; branchIndex < branches.length; branchIndex += BITBUCKET_REQUEST_BATCH_SIZE) {
255
+ updated.push(...await Promise.all(branches.slice(branchIndex, branchIndex + BITBUCKET_REQUEST_BATCH_SIZE).map(({ base, branch }) => fetchBranchUpdatedAt(base, branch))));
256
+ }
257
+ return { name, updatedAt: Math.max(...updated.map((item) => item.updatedAt)), rank: Math.min(...updated.map((item) => item.rank)) };
258
+ }));
259
+ commonBranches.push(...batch);
260
+ }
250
261
  commonBranches.sort((a, b) => b.updatedAt - a.updatedAt || a.rank - b.rank || a.name.localeCompare(b.name));
251
262
  res.json(commonBranches.map(({ name, updatedAt }) => ({ name, updatedAt: updatedAt ? new Date(updatedAt).toISOString() : null })));
252
263
  } catch (error) {
@@ -10,6 +10,7 @@ import { readPackageUpgradeData } from '../repositories/package-upgrade.js';
10
10
  import { getGitRemoteUrl } from '../services/git.js';
11
11
 
12
12
  const router = Router();
13
+ const CONFLICT_CHECK_BATCH_SIZE = 5;
13
14
  const execFileAsync = promisify(execFile);
14
15
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
15
16
  const client = axios.create({ httpsAgent, validateStatus: () => true });
@@ -335,35 +336,39 @@ router.get('/my-conflicts', async (_req, res) => {
335
336
  }
336
337
  });
337
338
 
338
- const conflicts = await Promise.all(values.map(async (pr) => {
339
- const project = pr.toRef?.repository?.project?.key;
340
- const repository = pr.toRef?.repository?.slug;
341
- if (!project || !repository || !pr.id) return null;
342
- if (String(pr.description || '').match(/This branch resolves conflicts for Pull Request #\d+/i)) return null;
343
-
344
- const resolutionPr = resolutionPrs.get(pullRequestKey(project, repository, pr.id));
345
- const baseConflict = {
346
- id: pr.id,
347
- title: pr.title || `Pull Request #${pr.id}`,
348
- repositoryName: pr.toRef?.repository?.name || repository,
349
- sourceBranch: pr.fromRef?.displayId || pr.fromRef?.id,
350
- targetBranch: pr.toRef?.displayId || pr.toRef?.id,
351
- updatedDate: pr.updatedDate,
352
- url: webPullRequestUrl(host, pr)
353
- };
354
- if (resolutionPr) {
355
- return {
356
- ...baseConflict,
357
- resolutionPrUrl: webPullRequestUrl(host, resolutionPr),
358
- resolutionPrId: resolutionPr.id
339
+ const conflicts = [];
340
+ for (let index = 0; index < values.length; index += CONFLICT_CHECK_BATCH_SIZE) {
341
+ const batch = await Promise.all(values.slice(index, index + CONFLICT_CHECK_BATCH_SIZE).map(async (pr) => {
342
+ const project = pr.toRef?.repository?.project?.key;
343
+ const repository = pr.toRef?.repository?.slug;
344
+ if (!project || !repository || !pr.id) return null;
345
+ if (String(pr.description || '').match(/This branch resolves conflicts for Pull Request #\d+/i)) return null;
346
+
347
+ const resolutionPr = resolutionPrs.get(pullRequestKey(project, repository, pr.id));
348
+ const baseConflict = {
349
+ id: pr.id,
350
+ title: pr.title || `Pull Request #${pr.id}`,
351
+ repositoryName: pr.toRef?.repository?.name || repository,
352
+ sourceBranch: pr.fromRef?.displayId || pr.fromRef?.id,
353
+ targetBranch: pr.toRef?.displayId || pr.toRef?.id,
354
+ updatedDate: pr.updatedDate,
355
+ url: webPullRequestUrl(host, pr)
359
356
  };
360
- }
357
+ if (resolutionPr) {
358
+ return {
359
+ ...baseConflict,
360
+ resolutionPrUrl: webPullRequestUrl(host, resolutionPr),
361
+ resolutionPrId: resolutionPr.id
362
+ };
363
+ }
361
364
 
362
- const mergeUrl = `https://${host}/rest/api/latest/projects/${encodeURIComponent(project)}/repos/${encodeURIComponent(repository)}/pull-requests/${pr.id}/merge`;
363
- const mergeResponse = await client.get(mergeUrl, { headers: authHeaders() });
364
- if (mergeResponse.status < 200 || mergeResponse.status >= 300 || !mergeResponse.data?.conflicted) return null;
365
- return baseConflict;
366
- }));
365
+ const mergeUrl = `https://${host}/rest/api/latest/projects/${encodeURIComponent(project)}/repos/${encodeURIComponent(repository)}/pull-requests/${pr.id}/merge`;
366
+ const mergeResponse = await client.get(mergeUrl, { headers: authHeaders() });
367
+ if (mergeResponse.status < 200 || mergeResponse.status >= 300 || !mergeResponse.data?.conflicted) return null;
368
+ return baseConflict;
369
+ }));
370
+ conflicts.push(...batch);
371
+ }
367
372
  const sorted = conflicts.filter(Boolean).sort((a, b) => (b.updatedDate || 0) - (a.updatedDate || 0));
368
373
  const unresolved = sorted.filter((pr) => !pr.resolutionPrUrl);
369
374
  const resolved = sorted.filter((pr) => pr.resolutionPrUrl);