standup-mr 0.3.0 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@ All notable changes to standup-mr are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.2] - 2026-09-01
8
+
9
+ Metadata only — no functional change, and nothing to do if you are already on
10
+ 0.3.1.
11
+
12
+ ### Added
13
+
14
+ - A project icon (`assets/`), referenced from `server.json` so the MCP
15
+ Registry and directories can show it. SVG plus 512x512 and 400x400 PNGs.
16
+
17
+ ### Fixed
18
+
19
+ - The release workflow no longer fails when the version being tagged is
20
+ already on npm; the publish step checks first and exits cleanly.
21
+
22
+ ## [0.3.1] - 2026-09-01
23
+
24
+ ### Fixed
25
+
26
+ - One flaky sub-request no longer costs the whole note. A run fans out to
27
+ several requests per merge request — pull detail, check runs, reviews, and
28
+ for a red pipeline the run, the job and its log — and any one of them
29
+ returning a 502 aborted the entire standup. Server errors (5xx) and dropped
30
+ connections are now retried twice with a short backoff before giving up. When
31
+ the retries run out the error is raised exactly as before: nothing is
32
+ swallowed. A rejected token (401), a refused resource (403), a rate limit
33
+ (429) and a missing resource (404) are not retried, because retrying them is
34
+ never the right answer.
35
+ - A blocker whose diagnosis could not be fetched is now reported as a blocker
36
+ with `job: "unknown"` and an error line reading
37
+ `diagnosis unavailable: <reason>`, instead of failing the run. The merge
38
+ request is genuinely blocked either way, and the log fetch already degraded
39
+ this way; the metadata calls around it now match. **A 401 is never degraded**
40
+ — a revoked or invalid token still fails loudly, which is the whole reason
41
+ these calls throw in the first place.
42
+
43
+ ### Added
44
+
45
+ - `mcpName` in `package.json`, for publication to the official MCP Registry.
46
+
7
47
  ## [0.3.0] - 2026-09-01
8
48
 
9
49
  ### Added
package/README.md CHANGED
@@ -188,6 +188,10 @@ that the package is missing.
188
188
  - **On GitHub, CI that reports only through the legacy commit-statuses API**
189
189
  — still how some vendors integrate — shows up as `pipelineMissing`. Check
190
190
  state is read from check-runs only.
191
+ - **A blocker whose diagnosis could not be fetched is still reported**, as
192
+ `job: "unknown"` with a `diagnosis unavailable: …` error line. The merge
193
+ request is blocked either way; only the explanation is missing. Server
194
+ errors are retried twice first, and a rejected token still fails the run.
191
195
 
192
196
  ## Upgrading from 0.1.x
193
197
 
@@ -218,9 +218,11 @@ function buildUrl(api, path, params) {
218
218
  return `${base}?${query.toString()}`;
219
219
  }
220
220
  var ApiError = class extends Error {
221
- constructor(message) {
221
+ status;
222
+ constructor(message, status) {
222
223
  super(message);
223
224
  this.name = "ApiError";
225
+ this.status = status;
224
226
  }
225
227
  };
226
228
  function remaining(response) {
@@ -244,27 +246,67 @@ function assertUsable(response, host) {
244
246
  if (response.ok || response.status === 404) return;
245
247
  if (response.status === 403 || response.status === 429) {
246
248
  if (remaining(response) === "0") {
247
- throw new ApiError(`${host} rate limit reached${resetAt(response)}.`);
249
+ throw new ApiError(`${host} rate limit reached${resetAt(response)}.`, response.status);
248
250
  }
249
251
  const retry = retryAfter(response);
250
- if (retry) throw new ApiError(`${host} rate limit reached${retry}.`);
252
+ if (retry) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
251
253
  }
252
254
  if (response.status === 401) {
253
255
  throw new ApiError(
254
- `${host} rejected the token (401). Check the token and its scopes.`
256
+ `${host} rejected the token (401). Check the token and its scopes.`,
257
+ 401
255
258
  );
256
259
  }
257
260
  if (response.status === 403) {
258
261
  throw new ApiError(
259
- `${host} refused the request (403). The token has no access to that resource.`
262
+ `${host} refused the request (403). The token has no access to that resource.`,
263
+ 403
260
264
  );
261
265
  }
262
- throw new ApiError(`${host} returned ${response.status}.`);
266
+ throw new ApiError(`${host} returned ${response.status}.`, response.status);
263
267
  }
264
268
  function unreachable(host, cause) {
265
269
  const detail = cause instanceof Error ? cause.message : String(cause);
266
270
  return new ApiError(`Could not reach ${host}: ${detail}`);
267
271
  }
272
+ var RETRY_ATTEMPTS = 3;
273
+ var RETRY_BACKOFF_MS = [250, 750];
274
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
275
+ async function sendWithRetry(fetchImpl, url, init, host, sleep = wait) {
276
+ let last;
277
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
278
+ if (attempt > 0) await sleep(RETRY_BACKOFF_MS[attempt - 1]);
279
+ const final = attempt === RETRY_ATTEMPTS - 1;
280
+ try {
281
+ const response = await fetchImpl(url, init);
282
+ if (response.status >= 500 && !final) continue;
283
+ return response;
284
+ } catch (cause) {
285
+ last = cause;
286
+ if (final) throw unreachable(host, cause);
287
+ }
288
+ }
289
+ throw unreachable(host, last);
290
+ }
291
+
292
+ // src/providers/base/diagnosis.ts
293
+ var DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
294
+ var UNKNOWN = "unknown";
295
+ function degradable(cause) {
296
+ return cause instanceof ApiError && cause.status !== 401;
297
+ }
298
+ function undiagnosed(mr, cause) {
299
+ return {
300
+ provider: mr.provider,
301
+ project: mr.project,
302
+ mr: mr.iid,
303
+ title: mr.title,
304
+ job: UNKNOWN,
305
+ stage: UNKNOWN,
306
+ url: mr.url,
307
+ errors: [`${DIAGNOSIS_UNAVAILABLE}: ${cause.message}`]
308
+ };
309
+ }
268
310
 
269
311
  // src/trace/trace.constants.ts
270
312
  var ANSI = /\x1b\[[0-9;]*[a-zA-Z]/g;
@@ -414,11 +456,12 @@ var GitHubProvider = class {
414
456
  };
415
457
  }
416
458
  async send(url, init) {
417
- try {
418
- return await this.fetchImpl(url, { headers: this.headers(), ...init });
419
- } catch (cause) {
420
- throw unreachable(this.host, cause);
421
- }
459
+ return await sendWithRetry(
460
+ this.fetchImpl,
461
+ url,
462
+ { headers: this.headers(), ...init },
463
+ this.host
464
+ );
422
465
  }
423
466
  async getJson(path, params) {
424
467
  const response = await this.send(buildUrl(this.api, path, params));
@@ -648,9 +691,17 @@ var GitHubProvider = class {
648
691
  }
649
692
  async getBlockers(mrs) {
650
693
  const red = mrs.filter((mr) => mr.pipeline === "failed");
651
- const diagnosed = await Promise.all(red.map((mr) => this.diagnose(mr)));
694
+ const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
652
695
  return diagnosed.filter((row) => row !== null);
653
696
  }
697
+ async tryDiagnose(mr) {
698
+ try {
699
+ return await this.diagnose(mr);
700
+ } catch (cause) {
701
+ if (degradable(cause)) return undiagnosed(mr, cause);
702
+ throw cause;
703
+ }
704
+ }
654
705
  };
655
706
 
656
707
  // src/providers/gitlab/gitlab.constants.ts
@@ -671,14 +722,12 @@ var GitLabProvider = class {
671
722
  this.fetchImpl = fetchImpl;
672
723
  }
673
724
  async send(url) {
674
- let response;
675
- try {
676
- response = await this.fetchImpl(url, {
677
- headers: { "PRIVATE-TOKEN": this.token }
678
- });
679
- } catch (cause) {
680
- throw unreachable(this.host, cause);
681
- }
725
+ const response = await sendWithRetry(
726
+ this.fetchImpl,
727
+ url,
728
+ { headers: { "PRIVATE-TOKEN": this.token } },
729
+ this.host
730
+ );
682
731
  assertUsable(response, this.host);
683
732
  return response;
684
733
  }
@@ -830,31 +879,36 @@ var GitLabProvider = class {
830
879
  );
831
880
  return rows.sort((a, b) => b.updated.localeCompare(a.updated));
832
881
  }
882
+ async diagnose(mr) {
883
+ const jobs = await this.getJson(
884
+ `projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
885
+ { per_page: PAGE_SIZE2 }
886
+ ) ?? [];
887
+ const job = jobs.find((j) => j.status === "failed");
888
+ if (!job) return null;
889
+ const trace = await this.getText(`projects/${mr.projectId}/jobs/${job.id}/trace`);
890
+ return {
891
+ provider: "gitlab",
892
+ project: mr.project,
893
+ mr: mr.iid,
894
+ title: mr.title,
895
+ job: job.name,
896
+ stage: job.stage,
897
+ url: mr.url,
898
+ errors: extractErrors(trace)
899
+ };
900
+ }
901
+ async tryDiagnose(mr) {
902
+ try {
903
+ return await this.diagnose(mr);
904
+ } catch (cause) {
905
+ if (degradable(cause)) return undiagnosed(mr, cause);
906
+ throw cause;
907
+ }
908
+ }
833
909
  async getBlockers(mrs) {
834
910
  const red = mrs.filter((mr) => mr.pipeline === "failed");
835
- const diagnosed = await Promise.all(
836
- red.map(async (mr) => {
837
- const jobs = await this.getJson(
838
- `projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
839
- { per_page: PAGE_SIZE2 }
840
- ) ?? [];
841
- const job = jobs.find((j) => j.status === "failed");
842
- if (!job) return null;
843
- const trace = await this.getText(
844
- `projects/${mr.projectId}/jobs/${job.id}/trace`
845
- );
846
- return {
847
- provider: "gitlab",
848
- project: mr.project,
849
- mr: mr.iid,
850
- title: mr.title,
851
- job: job.name,
852
- stage: job.stage,
853
- url: mr.url,
854
- errors: extractErrors(trace)
855
- };
856
- })
857
- );
911
+ const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
858
912
  return diagnosed.filter((row) => row !== null);
859
913
  }
860
914
  };
@@ -1103,6 +1157,12 @@ export {
1103
1157
  ApiError,
1104
1158
  assertUsable,
1105
1159
  unreachable,
1160
+ RETRY_ATTEMPTS,
1161
+ RETRY_BACKOFF_MS,
1162
+ sendWithRetry,
1163
+ DIAGNOSIS_UNAVAILABLE,
1164
+ degradable,
1165
+ undiagnosed,
1106
1166
  extractErrors,
1107
1167
  repoFromUrl,
1108
1168
  mapEvent,
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  connect,
6
6
  postWebhook,
7
7
  toMarkdown
8
- } from "./chunk-DL2P3XR6.js";
8
+ } from "./chunk-2X2XEEUY.js";
9
9
 
10
10
  // src/cli/cli.ts
11
11
  import { existsSync, readFileSync, realpathSync } from "fs";
package/dist/index.d.ts CHANGED
@@ -142,10 +142,19 @@ interface Provider {
142
142
 
143
143
  declare function buildUrl(api: string, path: string, params?: Record<string, string | number>): string;
144
144
  declare class ApiError extends Error {
145
- constructor(message: string);
145
+ readonly status?: number;
146
+ constructor(message: string, status?: number);
146
147
  }
147
148
  declare function assertUsable(response: Response, host: string): void;
148
149
  declare function unreachable(host: string, cause: unknown): ApiError;
150
+ declare const RETRY_ATTEMPTS = 3;
151
+ declare const RETRY_BACKOFF_MS: number[];
152
+ type Sleep = (ms: number) => Promise<void>;
153
+ declare function sendWithRetry(fetchImpl: FetchLike, url: string, init: RequestInit | undefined, host: string, sleep?: Sleep): Promise<Response>;
154
+
155
+ declare const DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
156
+ declare function degradable(cause: unknown): cause is ApiError;
157
+ declare function undiagnosed(mr: MergeRequest, cause: ApiError): Blocker;
149
158
 
150
159
  type Params$1 = Record<string, string | number>;
151
160
  declare class GitHubProvider implements Provider {
@@ -171,6 +180,7 @@ declare class GitHubProvider implements Provider {
171
180
  private checkRunBlocker;
172
181
  private diagnose;
173
182
  getBlockers(mrs: MergeRequest[]): Promise<Blocker[]>;
183
+ private tryDiagnose;
174
184
  }
175
185
 
176
186
  interface RawCommit {
@@ -247,6 +257,8 @@ declare class GitLabProvider implements Provider {
247
257
  getMyMrs(today: Date): Promise<MergeRequest[]>;
248
258
  private approvedByMe;
249
259
  getReviews(identity: Identity, today: Date): Promise<Review[]>;
260
+ private diagnose;
261
+ private tryDiagnose;
250
262
  getBlockers(mrs: MergeRequest[]): Promise<Blocker[]>;
251
263
  }
252
264
 
@@ -269,4 +281,4 @@ declare function buildReport(provider: Provider, today: Date, lang?: string, loo
269
281
 
270
282
  declare function extractErrors(rawTrace: string, limit?: number): string[];
271
283
 
272
- export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, extractErrors, ghHosts, ghToken, glabHosts, glabToken, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, repoFromUrl, resolveHost, resolveToken, toMarkdown, unreachable };
284
+ export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, DIAGNOSIS_UNAVAILABLE, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, RETRY_ATTEMPTS, RETRY_BACKOFF_MS, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, degradable, extractErrors, ghHosts, ghToken, glabHosts, glabToken, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toMarkdown, undiagnosed, unreachable };
package/dist/index.js CHANGED
@@ -2,10 +2,13 @@
2
2
  import {
3
3
  ApiError,
4
4
  ConfigError,
5
+ DIAGNOSIS_UNAVAILABLE,
5
6
  GITHUB_LABELS,
6
7
  GITLAB_LABELS,
7
8
  GitHubProvider,
8
9
  GitLabProvider,
10
+ RETRY_ATTEMPTS,
11
+ RETRY_BACKOFF_MS,
9
12
  STALE_DAYS,
10
13
  approvedBy,
11
14
  assertUsable,
@@ -15,6 +18,7 @@ import {
15
18
  classify,
16
19
  connect,
17
20
  countChangesRequested,
21
+ degradable,
18
22
  extractErrors,
19
23
  ghHosts,
20
24
  ghToken,
@@ -34,16 +38,21 @@ import {
34
38
  repoFromUrl,
35
39
  resolveHost,
36
40
  resolveToken,
41
+ sendWithRetry,
37
42
  toMarkdown,
43
+ undiagnosed,
38
44
  unreachable
39
- } from "./chunk-DL2P3XR6.js";
45
+ } from "./chunk-2X2XEEUY.js";
40
46
  export {
41
47
  ApiError,
42
48
  ConfigError,
49
+ DIAGNOSIS_UNAVAILABLE,
43
50
  GITHUB_LABELS,
44
51
  GITLAB_LABELS,
45
52
  GitHubProvider,
46
53
  GitLabProvider,
54
+ RETRY_ATTEMPTS,
55
+ RETRY_BACKOFF_MS,
47
56
  STALE_DAYS,
48
57
  approvedBy,
49
58
  assertUsable,
@@ -53,6 +62,7 @@ export {
53
62
  classify,
54
63
  connect,
55
64
  countChangesRequested,
65
+ degradable,
56
66
  extractErrors,
57
67
  ghHosts,
58
68
  ghToken,
@@ -72,6 +82,8 @@ export {
72
82
  repoFromUrl,
73
83
  resolveHost,
74
84
  resolveToken,
85
+ sendWithRetry,
75
86
  toMarkdown,
87
+ undiagnosed,
76
88
  unreachable
77
89
  };
@@ -243,9 +243,11 @@ function buildUrl(api, path, params) {
243
243
  return `${base}?${query.toString()}`;
244
244
  }
245
245
  var ApiError = class extends Error {
246
- constructor(message) {
246
+ status;
247
+ constructor(message, status) {
247
248
  super(message);
248
249
  this.name = "ApiError";
250
+ this.status = status;
249
251
  }
250
252
  };
251
253
  function remaining(response) {
@@ -269,27 +271,67 @@ function assertUsable(response, host) {
269
271
  if (response.ok || response.status === 404) return;
270
272
  if (response.status === 403 || response.status === 429) {
271
273
  if (remaining(response) === "0") {
272
- throw new ApiError(`${host} rate limit reached${resetAt(response)}.`);
274
+ throw new ApiError(`${host} rate limit reached${resetAt(response)}.`, response.status);
273
275
  }
274
276
  const retry = retryAfter(response);
275
- if (retry) throw new ApiError(`${host} rate limit reached${retry}.`);
277
+ if (retry) throw new ApiError(`${host} rate limit reached${retry}.`, response.status);
276
278
  }
277
279
  if (response.status === 401) {
278
280
  throw new ApiError(
279
- `${host} rejected the token (401). Check the token and its scopes.`
281
+ `${host} rejected the token (401). Check the token and its scopes.`,
282
+ 401
280
283
  );
281
284
  }
282
285
  if (response.status === 403) {
283
286
  throw new ApiError(
284
- `${host} refused the request (403). The token has no access to that resource.`
287
+ `${host} refused the request (403). The token has no access to that resource.`,
288
+ 403
285
289
  );
286
290
  }
287
- throw new ApiError(`${host} returned ${response.status}.`);
291
+ throw new ApiError(`${host} returned ${response.status}.`, response.status);
288
292
  }
289
293
  function unreachable(host, cause) {
290
294
  const detail = cause instanceof Error ? cause.message : String(cause);
291
295
  return new ApiError(`Could not reach ${host}: ${detail}`);
292
296
  }
297
+ var RETRY_ATTEMPTS = 3;
298
+ var RETRY_BACKOFF_MS = [250, 750];
299
+ var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
300
+ async function sendWithRetry(fetchImpl, url, init, host, sleep = wait) {
301
+ let last;
302
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
303
+ if (attempt > 0) await sleep(RETRY_BACKOFF_MS[attempt - 1]);
304
+ const final = attempt === RETRY_ATTEMPTS - 1;
305
+ try {
306
+ const response = await fetchImpl(url, init);
307
+ if (response.status >= 500 && !final) continue;
308
+ return response;
309
+ } catch (cause) {
310
+ last = cause;
311
+ if (final) throw unreachable(host, cause);
312
+ }
313
+ }
314
+ throw unreachable(host, last);
315
+ }
316
+
317
+ // src/providers/base/diagnosis.ts
318
+ var DIAGNOSIS_UNAVAILABLE = "diagnosis unavailable";
319
+ var UNKNOWN = "unknown";
320
+ function degradable(cause) {
321
+ return cause instanceof ApiError && cause.status !== 401;
322
+ }
323
+ function undiagnosed(mr, cause) {
324
+ return {
325
+ provider: mr.provider,
326
+ project: mr.project,
327
+ mr: mr.iid,
328
+ title: mr.title,
329
+ job: UNKNOWN,
330
+ stage: UNKNOWN,
331
+ url: mr.url,
332
+ errors: [`${DIAGNOSIS_UNAVAILABLE}: ${cause.message}`]
333
+ };
334
+ }
293
335
 
294
336
  // src/providers/github/github.constants.ts
295
337
  var PAGE_SIZE = 100;
@@ -398,11 +440,12 @@ var GitHubProvider = class {
398
440
  };
399
441
  }
400
442
  async send(url, init) {
401
- try {
402
- return await this.fetchImpl(url, { headers: this.headers(), ...init });
403
- } catch (cause) {
404
- throw unreachable(this.host, cause);
405
- }
443
+ return await sendWithRetry(
444
+ this.fetchImpl,
445
+ url,
446
+ { headers: this.headers(), ...init },
447
+ this.host
448
+ );
406
449
  }
407
450
  async getJson(path, params) {
408
451
  const response = await this.send(buildUrl(this.api, path, params));
@@ -632,9 +675,17 @@ var GitHubProvider = class {
632
675
  }
633
676
  async getBlockers(mrs) {
634
677
  const red = mrs.filter((mr) => mr.pipeline === "failed");
635
- const diagnosed = await Promise.all(red.map((mr) => this.diagnose(mr)));
678
+ const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
636
679
  return diagnosed.filter((row) => row !== null);
637
680
  }
681
+ async tryDiagnose(mr) {
682
+ try {
683
+ return await this.diagnose(mr);
684
+ } catch (cause) {
685
+ if (degradable(cause)) return undiagnosed(mr, cause);
686
+ throw cause;
687
+ }
688
+ }
638
689
  };
639
690
 
640
691
  // src/providers/gitlab/gitlab.constants.ts
@@ -655,14 +706,12 @@ var GitLabProvider = class {
655
706
  this.fetchImpl = fetchImpl;
656
707
  }
657
708
  async send(url) {
658
- let response;
659
- try {
660
- response = await this.fetchImpl(url, {
661
- headers: { "PRIVATE-TOKEN": this.token }
662
- });
663
- } catch (cause) {
664
- throw unreachable(this.host, cause);
665
- }
709
+ const response = await sendWithRetry(
710
+ this.fetchImpl,
711
+ url,
712
+ { headers: { "PRIVATE-TOKEN": this.token } },
713
+ this.host
714
+ );
666
715
  assertUsable(response, this.host);
667
716
  return response;
668
717
  }
@@ -814,31 +863,36 @@ var GitLabProvider = class {
814
863
  );
815
864
  return rows.sort((a, b) => b.updated.localeCompare(a.updated));
816
865
  }
866
+ async diagnose(mr) {
867
+ const jobs = await this.getJson(
868
+ `projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
869
+ { per_page: PAGE_SIZE2 }
870
+ ) ?? [];
871
+ const job = jobs.find((j) => j.status === "failed");
872
+ if (!job) return null;
873
+ const trace = await this.getText(`projects/${mr.projectId}/jobs/${job.id}/trace`);
874
+ return {
875
+ provider: "gitlab",
876
+ project: mr.project,
877
+ mr: mr.iid,
878
+ title: mr.title,
879
+ job: job.name,
880
+ stage: job.stage,
881
+ url: mr.url,
882
+ errors: extractErrors(trace)
883
+ };
884
+ }
885
+ async tryDiagnose(mr) {
886
+ try {
887
+ return await this.diagnose(mr);
888
+ } catch (cause) {
889
+ if (degradable(cause)) return undiagnosed(mr, cause);
890
+ throw cause;
891
+ }
892
+ }
817
893
  async getBlockers(mrs) {
818
894
  const red = mrs.filter((mr) => mr.pipeline === "failed");
819
- const diagnosed = await Promise.all(
820
- red.map(async (mr) => {
821
- const jobs = await this.getJson(
822
- `projects/${mr.projectId}/pipelines/${mr.pipelineId}/jobs`,
823
- { per_page: PAGE_SIZE2 }
824
- ) ?? [];
825
- const job = jobs.find((j) => j.status === "failed");
826
- if (!job) return null;
827
- const trace = await this.getText(
828
- `projects/${mr.projectId}/jobs/${job.id}/trace`
829
- );
830
- return {
831
- provider: "gitlab",
832
- project: mr.project,
833
- mr: mr.iid,
834
- title: mr.title,
835
- job: job.name,
836
- stage: job.stage,
837
- url: mr.url,
838
- errors: extractErrors(trace)
839
- };
840
- })
841
- );
895
+ const diagnosed = await Promise.all(red.map((mr) => this.tryDiagnose(mr)));
842
896
  return diagnosed.filter((row) => row !== null);
843
897
  }
844
898
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "standup-mr",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
+ "mcpName": "io.github.Jubstaaa/standup-mr",
4
5
  "description": "Standup notes from merge request state, not commit logs.",
5
6
  "keywords": [
6
7
  "standup",