@continuous-excellence/ze-great-dashboard-aws 0.18.4 → 0.20.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.
package/dist/lambda.mjs CHANGED
@@ -3124,7 +3124,7 @@ var require_timestamp = __commonJS({
3124
3124
  resolve: (str) => parseSexagesimal(str, false),
3125
3125
  stringify: stringifySexagesimal
3126
3126
  };
3127
- var timestamp2 = {
3127
+ var timestamp3 = {
3128
3128
  identify: (value) => value instanceof Date,
3129
3129
  default: true,
3130
3130
  tag: "tag:yaml.org,2002:timestamp",
@@ -3133,7 +3133,7 @@ var require_timestamp = __commonJS({
3133
3133
  // assumed to be 00:00:00Z (start of day, UTC).
3134
3134
  test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$"),
3135
3135
  resolve(str) {
3136
- const match2 = str.match(timestamp2.test);
3136
+ const match2 = str.match(timestamp3.test);
3137
3137
  if (!match2)
3138
3138
  throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
3139
3139
  const [, year2, month, day, hour, minute, second] = match2.map(Number);
@@ -3152,7 +3152,7 @@ var require_timestamp = __commonJS({
3152
3152
  };
3153
3153
  exports.floatTime = floatTime;
3154
3154
  exports.intTime = intTime;
3155
- exports.timestamp = timestamp2;
3155
+ exports.timestamp = timestamp3;
3156
3156
  }
3157
3157
  });
3158
3158
 
@@ -3172,7 +3172,7 @@ var require_schema3 = __commonJS({
3172
3172
  var omap = require_omap();
3173
3173
  var pairs = require_pairs();
3174
3174
  var set2 = require_set();
3175
- var timestamp2 = require_timestamp();
3175
+ var timestamp3 = require_timestamp();
3176
3176
  var schema = [
3177
3177
  map4.map,
3178
3178
  seq.seq,
@@ -3192,9 +3192,9 @@ var require_schema3 = __commonJS({
3192
3192
  omap.omap,
3193
3193
  pairs.pairs,
3194
3194
  set2.set,
3195
- timestamp2.intTime,
3196
- timestamp2.floatTime,
3197
- timestamp2.timestamp
3195
+ timestamp3.intTime,
3196
+ timestamp3.floatTime,
3197
+ timestamp3.timestamp
3198
3198
  ];
3199
3199
  exports.schema = schema;
3200
3200
  }
@@ -3219,7 +3219,7 @@ var require_tags = __commonJS({
3219
3219
  var pairs = require_pairs();
3220
3220
  var schema$2 = require_schema3();
3221
3221
  var set2 = require_set();
3222
- var timestamp2 = require_timestamp();
3222
+ var timestamp3 = require_timestamp();
3223
3223
  var schemas = /* @__PURE__ */ new Map([
3224
3224
  ["core", schema.schema],
3225
3225
  ["failsafe", [map4.map, seq.seq, string4.string]],
@@ -3233,11 +3233,11 @@ var require_tags = __commonJS({
3233
3233
  float: float.float,
3234
3234
  floatExp: float.floatExp,
3235
3235
  floatNaN: float.floatNaN,
3236
- floatTime: timestamp2.floatTime,
3236
+ floatTime: timestamp3.floatTime,
3237
3237
  int: int2.int,
3238
3238
  intHex: int2.intHex,
3239
3239
  intOct: int2.intOct,
3240
- intTime: timestamp2.intTime,
3240
+ intTime: timestamp3.intTime,
3241
3241
  map: map4.map,
3242
3242
  merge: merge3.merge,
3243
3243
  null: _null4.nullTag,
@@ -3245,7 +3245,7 @@ var require_tags = __commonJS({
3245
3245
  pairs: pairs.pairs,
3246
3246
  seq: seq.seq,
3247
3247
  set: set2.set,
3248
- timestamp: timestamp2.timestamp
3248
+ timestamp: timestamp3.timestamp
3249
3249
  };
3250
3250
  var coreKnownTags = {
3251
3251
  "tag:yaml.org,2002:binary": binary.binary,
@@ -3253,7 +3253,7 @@ var require_tags = __commonJS({
3253
3253
  "tag:yaml.org,2002:omap": omap.omap,
3254
3254
  "tag:yaml.org,2002:pairs": pairs.pairs,
3255
3255
  "tag:yaml.org,2002:set": set2.set,
3256
- "tag:yaml.org,2002:timestamp": timestamp2.timestamp
3256
+ "tag:yaml.org,2002:timestamp": timestamp3.timestamp
3257
3257
  };
3258
3258
  function getTags(customTags, schemaName, addMergeTag) {
3259
3259
  const schemaTags = schemas.get(schemaName);
@@ -10643,12 +10643,12 @@ var init_schema_date_utils = __esm({
10643
10643
  [, month, day, hour, minute, second, fraction, year2] = matches;
10644
10644
  }
10645
10645
  if (year2 && second) {
10646
- const timestamp2 = Date.UTC(Number(year2), months.indexOf(month), Number(day), Number(hour), Number(minute), Number(second), fraction ? Math.round(parseFloat(`0.${fraction}`) * 1e3) : 0);
10646
+ const timestamp3 = Date.UTC(Number(year2), months.indexOf(month), Number(day), Number(hour), Number(minute), Number(second), fraction ? Math.round(parseFloat(`0.${fraction}`) * 1e3) : 0);
10647
10647
  range(day, 1, 31);
10648
10648
  range(hour, 0, 23);
10649
10649
  range(minute, 0, 59);
10650
10650
  range(second, 0, 60);
10651
- const date6 = new Date(timestamp2);
10651
+ const date6 = new Date(timestamp3);
10652
10652
  date6.setUTCFullYear(Number(year2));
10653
10653
  return date6;
10654
10654
  }
@@ -17638,14 +17638,14 @@ var init_DefaultRateLimiter = __esm({
17638
17638
  this.availableTokens = this.availableTokens - amount;
17639
17639
  }
17640
17640
  refillTokenBucket() {
17641
- const timestamp2 = this.getCurrentTimeInSeconds();
17641
+ const timestamp3 = this.getCurrentTimeInSeconds();
17642
17642
  if (!this.lastTimestamp) {
17643
- this.lastTimestamp = timestamp2;
17643
+ this.lastTimestamp = timestamp3;
17644
17644
  return;
17645
17645
  }
17646
- const fillAmount = (timestamp2 - this.lastTimestamp) * this.fillRate;
17646
+ const fillAmount = (timestamp3 - this.lastTimestamp) * this.fillRate;
17647
17647
  this.availableTokens = Math.min(this.maxCapacity, this.availableTokens + fillAmount);
17648
- this.lastTimestamp = timestamp2;
17648
+ this.lastTimestamp = timestamp3;
17649
17649
  }
17650
17650
  calculateTimeWindow() {
17651
17651
  this.timeWindow = this.getPrecise(Math.pow(this.lastMaxRate * (1 - this.beta) / this.scaleConstant, 1 / 3));
@@ -17653,8 +17653,8 @@ var init_DefaultRateLimiter = __esm({
17653
17653
  cubicThrottle(rateToUse) {
17654
17654
  return this.getPrecise(rateToUse * this.beta);
17655
17655
  }
17656
- cubicSuccess(timestamp2) {
17657
- return this.getPrecise(this.scaleConstant * Math.pow(timestamp2 - this.lastThrottleTime - this.timeWindow, 3) + this.lastMaxRate);
17656
+ cubicSuccess(timestamp3) {
17657
+ return this.getPrecise(this.scaleConstant * Math.pow(timestamp3 - this.lastThrottleTime - this.timeWindow, 3) + this.lastMaxRate);
17658
17658
  }
17659
17659
  enableTokenBucket() {
17660
17660
  this.enabled = true;
@@ -49137,8 +49137,8 @@ var handle = (app, { isContentTypeBinary } = { isContentTypeBinary: void 0 }) =>
49137
49137
  requestContext = getRequestContext(event);
49138
49138
  } catch (error52) {
49139
49139
  console.error("Error processing request:", error52);
49140
- const errorResponse2 = error52 instanceof TypeError ? new Response("Invalid request", { status: 400 }) : new Response("Internal Server Error", { status: 500 });
49141
- return processor.createResult(event, errorResponse2, { isContentTypeBinary });
49140
+ const errorResponse3 = error52 instanceof TypeError ? new Response("Invalid request", { status: 400 }) : new Response("Internal Server Error", { status: 500 });
49141
+ return processor.createResult(event, errorResponse3, { isContentTypeBinary });
49142
49142
  }
49143
49143
  const res = await app.fetch(req, {
49144
49144
  event,
@@ -64040,14 +64040,25 @@ var githubActionsSourceSchema = external_exports.looseObject({
64040
64040
  branch: external_exports.string().min(1).optional(),
64041
64041
  ...sourceAuthenticationSchema
64042
64042
  }).superRefine(exactlyOneGithubAuthenticationMode);
64043
+ var azureDevOpsSourceSchema = external_exports.looseObject({
64044
+ type: external_exports.literal("azure-devops"),
64045
+ /** Azure DevOps organization name, used as one URL path segment. */
64046
+ organization: external_exports.string().regex(/^[^/\s]+$/, "must be an organization name"),
64047
+ project: external_exports.string().min(1),
64048
+ /** The branch whose build health the dashboard represents. */
64049
+ branch: external_exports.string().min(1).optional(),
64050
+ /** Azure DevOps Build API access is authenticated with a read-scoped PAT. */
64051
+ token_env: external_exports.string().min(1)
64052
+ });
64043
64053
  var sourceSchema = external_exports.looseObject({ type: external_exports.string().min(1), ...sourceAuthenticationSchema }).superRefine(exactlyOneGithubAuthenticationMode);
64044
64054
  function credentialEnvironmentNames(source) {
64055
+ const githubApp = githubAppSchema.safeParse(source.github_app);
64045
64056
  return [
64046
64057
  ...source.token_env ? [source.token_env] : [],
64047
- ...source.github_app ? [
64048
- source.github_app.app_id_env,
64049
- source.github_app.private_key_env,
64050
- source.github_app.installation_id_env
64058
+ ...githubApp.success ? [
64059
+ githubApp.data.app_id_env,
64060
+ githubApp.data.private_key_env,
64061
+ githubApp.data.installation_id_env
64051
64062
  ] : []
64052
64063
  ];
64053
64064
  }
@@ -64085,6 +64096,33 @@ var boardConfigSchema = external_exports.object({
64085
64096
  sources: external_exports.record(external_exports.string().min(1), sourceSchema).default({}),
64086
64097
  boards: external_exports.record(external_exports.string().min(1), boardSchema),
64087
64098
  auth: authSchema.optional()
64099
+ }).superRefine((config2, ctx) => {
64100
+ for (const [sourceName, source] of Object.entries(config2.sources)) {
64101
+ if (source.type !== "azure-devops") continue;
64102
+ const parsedSource = azureDevOpsSourceSchema.safeParse(source);
64103
+ if (!parsedSource.success) {
64104
+ for (const issue2 of parsedSource.error.issues) {
64105
+ ctx.addIssue({
64106
+ code: "custom",
64107
+ path: ["sources", sourceName, ...issue2.path],
64108
+ message: issue2.message
64109
+ });
64110
+ }
64111
+ }
64112
+ }
64113
+ for (const [boardName, board] of Object.entries(config2.boards)) {
64114
+ board.panels.forEach((panel, panelIndex) => {
64115
+ const source = panel.source ? config2.sources[panel.source] : void 0;
64116
+ if (panel.type !== "pipeline-status" || source?.type !== "azure-devops") return;
64117
+ if (typeof panel.pipeline !== "number" || !Number.isInteger(panel.pipeline) || panel.pipeline <= 0) {
64118
+ ctx.addIssue({
64119
+ code: "custom",
64120
+ path: ["boards", boardName, "panels", panelIndex, "pipeline"],
64121
+ message: "Azure DevOps pipeline-status panels require a positive numeric pipeline definition id"
64122
+ });
64123
+ }
64124
+ });
64125
+ }
64088
64126
  });
64089
64127
 
64090
64128
  // packages/shared/src/board-config-modeline.ts
@@ -64129,15 +64167,13 @@ var clientEnvSchema = external_exports.object({
64129
64167
  proxyPath: external_exports.string().min(1),
64130
64168
  /** Which board this is, resolved server-side so the client parses no URLs. */
64131
64169
  board: external_exports.string().min(1),
64132
- /** The client version being served, so two published versions are visibly distinguishable. */
64133
- clientVersion: external_exports.string().min(1),
64134
64170
  /** Omitted entirely when unauthenticated — presence is what turns auth on. */
64135
64171
  auth: external_exports.object({
64136
64172
  issuer: external_exports.string().min(1),
64137
64173
  clientId: external_exports.string().min(1).optional()
64138
64174
  }).optional()
64139
64175
  });
64140
- var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true, clientVersion: true });
64176
+ var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true });
64141
64177
 
64142
64178
  // packages/shared/src/envelope.ts
64143
64179
  var errorKindSchema = external_exports.enum([
@@ -64211,7 +64247,32 @@ var pullRequestHealthSchema = external_exports.object({
64211
64247
  status: pipelineStatusSchema.shape.status,
64212
64248
  summary: external_exports.string().min(1),
64213
64249
  workflows: external_exports.array(pullRequestHealthItemSchema),
64214
- pullRequests: external_exports.array(pullRequestHealthItemSchema)
64250
+ pullRequests: external_exports.array(pullRequestHealthItemSchema),
64251
+ /** Missing component reads remain visible even when the evidence we did get is reassuring. */
64252
+ incompleteObservations: external_exports.array(external_exports.object({ label: external_exports.string().min(1), message: external_exports.string().min(1) })).optional(),
64253
+ /** The latest component reading, disclosed beside the oldest aggregate evidence. */
64254
+ newestObservedAt: external_exports.iso.datetime().optional()
64255
+ });
64256
+ var pullRequestCandidateSchema = external_exports.object({
64257
+ number: external_exports.number().int().positive(),
64258
+ branch: external_exports.string().min(1),
64259
+ link: external_exports.url()
64260
+ });
64261
+ var pullRequestCandidatesSchema = external_exports.object({
64262
+ type: external_exports.literal("pull-request-candidates"),
64263
+ pullRequests: external_exports.array(pullRequestCandidateSchema),
64264
+ /** GitHub's first page is a deliberate bounded observation, not proof that no later PR exists. */
64265
+ truncated: external_exports.boolean().optional()
64266
+ });
64267
+ var pullRequestWorkflowObservationSchema = external_exports.object({
64268
+ type: external_exports.literal("pull-request-workflow"),
64269
+ workflow: external_exports.string().min(1),
64270
+ item: pullRequestHealthItemSchema
64271
+ });
64272
+ var pullRequestBuildObservationSchema = external_exports.object({
64273
+ type: external_exports.literal("pull-request-build"),
64274
+ branch: external_exports.string().min(1),
64275
+ item: pullRequestHealthItemSchema
64215
64276
  });
64216
64277
  var httpValueSchema = external_exports.object({
64217
64278
  type: external_exports.literal("http-value"),
@@ -66400,6 +66461,275 @@ var Hono2 = class extends Hono {
66400
66461
  // packages/server/src/app.ts
66401
66462
  var import_yaml = __toESM(require_dist(), 1);
66402
66463
 
66464
+ // packages/server/src/upstream.ts
66465
+ function forwardValidators(requestHeaders, upstreamHeaders) {
66466
+ for (const name of ["if-none-match", "if-modified-since"]) {
66467
+ const value = requestHeaders.get(name);
66468
+ if (value) upstreamHeaders.set(name, value);
66469
+ }
66470
+ }
66471
+ function observedAt(upstreamDate) {
66472
+ const parsed = upstreamDate ? new Date(upstreamDate) : void 0;
66473
+ return parsed && !Number.isNaN(parsed.valueOf()) ? parsed.toISOString() : (/* @__PURE__ */ new Date()).toISOString();
66474
+ }
66475
+ function upstreamErrorEnvelope(args) {
66476
+ return {
66477
+ panelId: args.panelId,
66478
+ state: "error",
66479
+ observedAt: observedAt(args.upstreamDate),
66480
+ link: args.link,
66481
+ error: {
66482
+ kind: args.kind,
66483
+ message: args.error instanceof Error ? args.error.message : String(args.error)
66484
+ }
66485
+ };
66486
+ }
66487
+ function upstreamErrorResponse(envelope) {
66488
+ return new Response(JSON.stringify(envelope), { status: 200 });
66489
+ }
66490
+ function upstreamErrorKind(status) {
66491
+ if (status === 401 || status === 403) return "unauthorized";
66492
+ if (status === 404) return "not-found";
66493
+ return "upstream-error";
66494
+ }
66495
+ async function adapterRouteResponse(result) {
66496
+ const headers = passthroughHeaders(result.response.headers);
66497
+ if (result.response.status === 304) return new Response(null, { status: 304, headers });
66498
+ const envelope = result.envelope ?? JSON.parse(await result.response.text());
66499
+ headers.set("content-type", "application/json; charset=utf-8");
66500
+ return new Response(JSON.stringify(envelope), { status: 200, headers });
66501
+ }
66502
+ function passthroughHeaders(upstream) {
66503
+ const headers = new Headers();
66504
+ for (const name of ["cache-control", "etag", "last-modified", "date", "vary"]) {
66505
+ const value = upstream.get(name);
66506
+ if (value) headers.set(name, value);
66507
+ }
66508
+ return headers;
66509
+ }
66510
+
66511
+ // packages/server/src/adapters/azure-devops.ts
66512
+ var pipelinePanelSchema = external_exports.object({
66513
+ id: external_exports.string().min(1),
66514
+ type: external_exports.literal("pipeline-status"),
66515
+ source: external_exports.string().min(1),
66516
+ pipeline: external_exports.number().int().positive()
66517
+ });
66518
+ var buildSchema = external_exports.object({
66519
+ id: external_exports.number().int().positive(),
66520
+ buildNumber: external_exports.string().min(1),
66521
+ status: external_exports.string().nullable().optional(),
66522
+ result: external_exports.string().nullable().optional(),
66523
+ sourceBranch: external_exports.string().min(1).nullable().optional(),
66524
+ startTime: external_exports.string().nullable().optional(),
66525
+ finishTime: external_exports.string().nullable().optional(),
66526
+ lastChangedDate: external_exports.string().nullable().optional()
66527
+ });
66528
+ var buildsSchema = external_exports.object({ value: external_exports.array(buildSchema) });
66529
+ var timelineSchema = external_exports.object({
66530
+ records: external_exports.array(
66531
+ external_exports.object({
66532
+ id: external_exports.string().min(1),
66533
+ parentId: external_exports.string().min(1).nullable().optional(),
66534
+ type: external_exports.string().min(1),
66535
+ name: external_exports.string().min(1),
66536
+ state: external_exports.string().nullable().optional()
66537
+ })
66538
+ )
66539
+ });
66540
+ function permittedAzureDevOpsCalls(panel, source) {
66541
+ const parsedPanel = pipelinePanelSchema.parse(panel);
66542
+ const parsedSource = azureDevOpsSourceSchema.parse(source);
66543
+ return [buildsCall(parsedPanel.pipeline, parsedSource)];
66544
+ }
66545
+ async function fetchAzureDevOpsPipeline(args) {
66546
+ const panel = pipelinePanelSchema.parse(args.panel);
66547
+ const source = azureDevOpsSourceSchema.parse(args.source);
66548
+ const token = args.credentials.get(source.token_env);
66549
+ if (!token) {
66550
+ return {
66551
+ response: errorResponse(
66552
+ errorEnvelope(
66553
+ panel.id,
66554
+ "unauthorized",
66555
+ `Credential ${source.token_env} is not available.`,
66556
+ sourceLink(panel.pipeline, source)
66557
+ )
66558
+ )
66559
+ };
66560
+ }
66561
+ const call = buildsCall(panel.pipeline, source);
66562
+ copyValidators(args.requestHeaders, call.headers);
66563
+ call.headers.set("authorization", `Basic ${btoa(`:${token}`)}`);
66564
+ let upstream;
66565
+ try {
66566
+ upstream = await args.fetcher(call.url, { headers: call.headers });
66567
+ } catch (error52) {
66568
+ return {
66569
+ response: errorResponse(
66570
+ errorEnvelope(panel.id, "unreachable", error52, sourceLink(panel.pipeline, source))
66571
+ )
66572
+ };
66573
+ }
66574
+ if (upstream.status === 304) return { response: upstream };
66575
+ if (!upstream.ok) {
66576
+ return {
66577
+ response: errorResponse(
66578
+ errorEnvelope(
66579
+ panel.id,
66580
+ errorKind(upstream.status),
66581
+ `${upstream.status} ${upstream.statusText}`,
66582
+ sourceLink(panel.pipeline, source),
66583
+ observedAt2(upstream.headers.get("date"))
66584
+ )
66585
+ )
66586
+ };
66587
+ }
66588
+ try {
66589
+ const build = buildsSchema.parse(await upstream.json()).value[0];
66590
+ if (!build) {
66591
+ return {
66592
+ response: errorResponse(
66593
+ errorEnvelope(
66594
+ panel.id,
66595
+ "no-runs",
66596
+ source.branch ? `No builds found for branch "${source.branch}". Check the source's branch setting.` : "No builds found.",
66597
+ sourceLink(panel.pipeline, source),
66598
+ observedAt2(upstream.headers.get("date"))
66599
+ )
66600
+ )
66601
+ };
66602
+ }
66603
+ const signal = {
66604
+ type: "pipeline-status",
66605
+ status: normalizeStatus(build.status, build.result),
66606
+ rawStatus: build.result ?? build.status ?? "unknown",
66607
+ name: build.buildNumber,
66608
+ ...build.sourceBranch ?? source.branch ? { branch: build.sourceBranch ?? source.branch } : {},
66609
+ ...timestamp(build.startTime) ? { runStartedAt: timestamp(build.startTime) } : {},
66610
+ ...timestamp(build.lastChangedDate) ? { sourceUpdatedAt: timestamp(build.lastChangedDate) } : {},
66611
+ sourceRunId: String(build.id),
66612
+ ...completedBuildDuration(build)
66613
+ };
66614
+ if (signal.status === "running") {
66615
+ const activity = await activeAzureDevOpsActivity({
66616
+ buildId: build.id,
66617
+ source,
66618
+ fetcher: args.fetcher,
66619
+ authorization: call.headers.get("authorization") ?? ""
66620
+ });
66621
+ if (activity) signal.activity = activity;
66622
+ }
66623
+ return {
66624
+ envelope: {
66625
+ panelId: panel.id,
66626
+ state: "ok",
66627
+ observedAt: observedAt2(upstream.headers.get("date")),
66628
+ link: buildLink(build.id, source),
66629
+ signal
66630
+ },
66631
+ response: upstream
66632
+ };
66633
+ } catch (error52) {
66634
+ return {
66635
+ response: errorResponse(
66636
+ errorEnvelope(
66637
+ panel.id,
66638
+ "upstream-error",
66639
+ error52,
66640
+ sourceLink(panel.pipeline, source),
66641
+ observedAt2(upstream.headers.get("date"))
66642
+ )
66643
+ )
66644
+ };
66645
+ }
66646
+ }
66647
+ function buildsCall(definitionId, source) {
66648
+ const url2 = apiUrl(source, "build/builds");
66649
+ url2.searchParams.set("definitions", String(definitionId));
66650
+ if (source.branch) url2.searchParams.set("branchName", adoBranch(source.branch));
66651
+ url2.searchParams.set("queryOrder", "queueTimeDescending");
66652
+ url2.searchParams.set("$top", "1");
66653
+ return { url: url2.toString(), headers: new Headers({ accept: "application/json" }) };
66654
+ }
66655
+ function timelineCall(buildId, source) {
66656
+ return {
66657
+ url: apiUrl(source, `build/builds/${buildId}/timeline`).toString(),
66658
+ headers: new Headers({ accept: "application/json" })
66659
+ };
66660
+ }
66661
+ function apiUrl(source, suffix) {
66662
+ const url2 = new URL(
66663
+ `https://dev.azure.com/${encodeURIComponent(source.organization)}/${encodeURIComponent(source.project)}/_apis/${suffix}`
66664
+ );
66665
+ url2.searchParams.set("api-version", "7.1");
66666
+ return url2;
66667
+ }
66668
+ function adoBranch(branch) {
66669
+ return branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
66670
+ }
66671
+ async function activeAzureDevOpsActivity(args) {
66672
+ const call = timelineCall(args.buildId, args.source);
66673
+ call.headers.set("authorization", args.authorization);
66674
+ try {
66675
+ const response = await args.fetcher(call.url, { headers: call.headers });
66676
+ if (!response.ok) return void 0;
66677
+ const records = timelineSchema.parse(await response.json()).records;
66678
+ const active = records.find((record2) => record2.state === "inProgress");
66679
+ if (!active) return void 0;
66680
+ const parent = active.parentId ? records.find((record2) => record2.id === active.parentId) : void 0;
66681
+ const kind = active.type.toLowerCase();
66682
+ return {
66683
+ kind: kind === "stage" ? "stage" : kind === "job" ? "job" : "step",
66684
+ name: active.name,
66685
+ ...parent ? { parent: parent.name } : {}
66686
+ };
66687
+ } catch {
66688
+ return void 0;
66689
+ }
66690
+ }
66691
+ function normalizeStatus(status, result) {
66692
+ if (status === "notStarted" || status === "postponed" || status === "inProgress" || status === "cancelling")
66693
+ return "running";
66694
+ if (status !== "completed") return "unknown";
66695
+ if (result === "succeeded") return "passed";
66696
+ if (result === "failed") return "failed";
66697
+ if (result === "canceled") return "cancelled";
66698
+ return "unknown";
66699
+ }
66700
+ function completedBuildDuration(build) {
66701
+ const startedAt = timestamp(build.startTime);
66702
+ const finishedAt = timestamp(build.finishTime);
66703
+ if (!startedAt || !finishedAt) return {};
66704
+ const durationMs = new Date(finishedAt).valueOf() - new Date(startedAt).valueOf();
66705
+ return Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {};
66706
+ }
66707
+ function timestamp(value) {
66708
+ if (!value || !external_exports.iso.datetime().safeParse(value).success) return void 0;
66709
+ return Number.isFinite(new Date(value).valueOf()) ? value : void 0;
66710
+ }
66711
+ function copyValidators(requestHeaders, upstreamHeaders) {
66712
+ forwardValidators(requestHeaders, upstreamHeaders);
66713
+ }
66714
+ function buildLink(buildId, source) {
66715
+ return `https://dev.azure.com/${encodeURIComponent(source.organization)}/${encodeURIComponent(source.project)}/_build/results?buildId=${buildId}`;
66716
+ }
66717
+ function sourceLink(definitionId, source) {
66718
+ return `https://dev.azure.com/${encodeURIComponent(source.organization)}/${encodeURIComponent(source.project)}/_build?definitionId=${definitionId}`;
66719
+ }
66720
+ function errorResponse(envelope) {
66721
+ return upstreamErrorResponse(envelope);
66722
+ }
66723
+ function errorEnvelope(panelId, kind, error52, link, date6) {
66724
+ return upstreamErrorEnvelope({ panelId, kind, error: error52, link, upstreamDate: date6 });
66725
+ }
66726
+ function errorKind(status) {
66727
+ return upstreamErrorKind(status);
66728
+ }
66729
+ function observedAt2(value) {
66730
+ return observedAt(value);
66731
+ }
66732
+
66403
66733
  // packages/server/src/credentials.ts
66404
66734
  var import_client_secrets_manager = __toESM(require_dist_cjs16(), 1);
66405
66735
  var import_client_ssm = __toESM(require_dist_cjs17(), 1);
@@ -66509,10 +66839,7 @@ function createGithubClient(credentials, now = Date.now) {
66509
66839
  }
66510
66840
  function githubHeaders(requestHeaders) {
66511
66841
  const headers = new Headers({ accept: "application/vnd.github+json" });
66512
- for (const name of ["if-none-match", "if-modified-since"]) {
66513
- const value = requestHeaders.get(name);
66514
- if (value) headers.set(name, value);
66515
- }
66842
+ forwardValidators(requestHeaders, headers);
66516
66843
  return headers;
66517
66844
  }
66518
66845
  async function exchangeInstallationToken(args) {
@@ -66565,7 +66892,7 @@ function base64url3(value) {
66565
66892
  }
66566
66893
 
66567
66894
  // packages/server/src/adapters/github-actions.ts
66568
- var pipelinePanelSchema = external_exports.object({
66895
+ var pipelinePanelSchema2 = external_exports.object({
66569
66896
  id: external_exports.string().min(1),
66570
66897
  type: external_exports.literal("pipeline-status"),
66571
66898
  source: external_exports.string().min(1),
@@ -66616,7 +66943,7 @@ var pullRequestsSchema = external_exports.array(
66616
66943
  })
66617
66944
  );
66618
66945
  function permittedGithubActionsCalls(panel, source, _credentials) {
66619
- const parsedPanel = pipelinePanelSchema.parse(panel);
66946
+ const parsedPanel = pipelinePanelSchema2.parse(panel);
66620
66947
  const parsedSource = githubActionsSourceSchema.parse(source);
66621
66948
  const url2 = new URL(
66622
66949
  `https://api.github.com/repos/${parsedSource.repo}/actions/workflows/${encodeURIComponent(parsedPanel.pipeline)}/runs`
@@ -66625,90 +66952,117 @@ function permittedGithubActionsCalls(panel, source, _credentials) {
66625
66952
  url2.searchParams.set("per_page", "1");
66626
66953
  return [{ url: url2.toString(), headers: new Headers() }];
66627
66954
  }
66628
- function permittedGithubActionsPullRequestHealthCalls(panel, source) {
66629
- const parsedPanel = pullRequestHealthPanelSchema.parse(panel);
66630
- const parsedSource = githubActionsSourceSchema.parse(source);
66631
- const calls = parsedPanel.update_workflows.map(
66632
- ({ workflow }) => githubRunsCall(parsedSource, workflow)
66955
+ function pullRequestHealthCapabilities(panel) {
66956
+ const parsed = pullRequestHealthPanelSchema.parse(panel);
66957
+ return {
66958
+ panel: parsed,
66959
+ configuredUpdateWorkflow(workflow) {
66960
+ return parsed.update_workflows.find((candidate) => candidate.workflow === workflow);
66961
+ },
66962
+ permitsBuildBranch(branch) {
66963
+ return parsed.update_workflows.some(
66964
+ ({ branch_prefixes }) => branch_prefixes.some((prefix) => branch.startsWith(prefix))
66965
+ );
66966
+ }
66967
+ };
66968
+ }
66969
+ async function fetchGithubActionsPullRequestCandidates(args) {
66970
+ const capabilities = pullRequestHealthCapabilities(args.panel);
66971
+ const panel = capabilities.panel;
66972
+ const source = githubActionsSourceSchema.parse(args.source);
66973
+ return githubObservation(
66974
+ args,
66975
+ source,
66976
+ pullRequestsCall(source, panel.base_branch, 1),
66977
+ (value, observedAt4) => {
66978
+ const page = pullRequestsSchema.parse(value);
66979
+ const pullRequests = page.filter(
66980
+ (pullRequest) => capabilities.permitsBuildBranch(pullRequest.head.ref)
66981
+ );
66982
+ return {
66983
+ panelId: panel.id,
66984
+ state: "ok",
66985
+ observedAt: observedAt4,
66986
+ link: sourceLinkForRepository(source),
66987
+ signal: {
66988
+ type: "pull-request-candidates",
66989
+ pullRequests: pullRequests.map((pullRequest) => ({
66990
+ number: pullRequest.number,
66991
+ branch: pullRequest.head.ref,
66992
+ link: pullRequest.html_url
66993
+ })),
66994
+ ...page.length === 100 ? { truncated: true } : {}
66995
+ }
66996
+ };
66997
+ }
66633
66998
  );
66634
- calls.push({
66635
- ...pullRequestsCall(parsedSource, parsedPanel.base_branch, 1)
66636
- });
66637
- return calls;
66638
66999
  }
66639
- async function fetchGithubActionsPullRequestHealth(args) {
66640
- const panel = pullRequestHealthPanelSchema.parse(args.panel);
67000
+ async function fetchGithubActionsUpdateWorkflow(args) {
67001
+ const capabilities = pullRequestHealthCapabilities(args.panel);
67002
+ const panel = capabilities.panel;
66641
67003
  const source = githubActionsSourceSchema.parse(args.source);
67004
+ const configured = capabilities.configuredUpdateWorkflow(args.workflow);
67005
+ if (!configured) throw new Error("Unconfigured update workflow");
67006
+ return githubObservation(
67007
+ args,
67008
+ source,
67009
+ githubRunsCall(source, configured.workflow, void 0, void 0, 100),
67010
+ (value, observedAt4) => {
67011
+ const run = runsSchema.parse(value).workflow_runs.find((candidate) => {
67012
+ const branch = candidate.head_branch;
67013
+ return typeof branch === "string" && configured.branch_prefixes.some((prefix) => branch.startsWith(prefix));
67014
+ });
67015
+ return {
67016
+ panelId: panel.id,
67017
+ state: "ok",
67018
+ observedAt: observedAt4,
67019
+ link: sourceLinkForRepository(source),
67020
+ signal: {
67021
+ type: "pull-request-workflow",
67022
+ workflow: configured.workflow,
67023
+ item: healthItem(configured.workflow, run, `Update workflow ${configured.workflow}`)
67024
+ }
67025
+ };
67026
+ }
67027
+ );
67028
+ }
67029
+ async function fetchGithubActionsPullRequestBuild(args) {
67030
+ const capabilities = pullRequestHealthCapabilities(args.panel);
67031
+ const panel = capabilities.panel;
67032
+ const source = githubActionsSourceSchema.parse(args.source);
67033
+ if (!capabilities.permitsBuildBranch(args.branch))
67034
+ throw new Error("Unpermitted pull request branch");
67035
+ return githubObservation(
67036
+ args,
67037
+ source,
67038
+ githubRunsCall(source, panel.build_workflow, args.branch, "pull_request"),
67039
+ (value, observedAt4) => {
67040
+ const run = runsSchema.parse(value).workflow_runs[0];
67041
+ return {
67042
+ panelId: panel.id,
67043
+ state: "ok",
67044
+ observedAt: observedAt4,
67045
+ link: sourceLinkForRepository(source),
67046
+ signal: {
67047
+ type: "pull-request-build",
67048
+ branch: args.branch,
67049
+ item: healthItem(args.branch, run, args.branch)
67050
+ }
67051
+ };
67052
+ }
67053
+ );
67054
+ }
67055
+ async function githubObservation(args, source, call, normalize) {
67056
+ let upstream;
66642
67057
  try {
66643
- const workflowResults = await Promise.all(
66644
- panel.update_workflows.map(async ({ workflow, branch_prefixes }) => ({
66645
- workflow,
66646
- run: await latestRun({
66647
- source,
66648
- workflow,
66649
- branchPrefixes: branch_prefixes,
66650
- requestHeaders: args.requestHeaders,
66651
- fetcher: args.fetcher,
66652
- githubClient: githubClientFor(args)
66653
- })
66654
- }))
66655
- );
66656
- const pullRequests = await openPullRequests({
66657
- source,
66658
- baseBranch: panel.base_branch,
66659
- requestHeaders: args.requestHeaders,
66660
- fetcher: args.fetcher,
66661
- githubClient: githubClientFor(args)
66662
- });
66663
- const matching = pullRequests.filter(
66664
- (pullRequest) => panel.update_workflows.some(
66665
- ({ branch_prefixes }) => branch_prefixes.some((prefix) => pullRequest.head.ref.startsWith(prefix))
66666
- )
66667
- );
66668
- const pullRequestResults = await Promise.all(
66669
- matching.map(async (pullRequest) => ({
66670
- pullRequest,
66671
- run: await latestRun({
66672
- source,
66673
- workflow: panel.build_workflow,
66674
- branch: pullRequest.head.ref,
66675
- event: "pull_request",
66676
- requestHeaders: args.requestHeaders,
66677
- fetcher: args.fetcher,
66678
- githubClient: githubClientFor(args)
66679
- })
66680
- }))
66681
- );
66682
- const workflows = workflowResults.map(
66683
- ({ workflow, run }) => healthItem(workflow, run, `Update workflow ${workflow}`)
66684
- );
66685
- const pullRequestItems = pullRequestResults.map(
66686
- ({ pullRequest, run }) => healthItem(`PR #${pullRequest.number}`, run, pullRequest.head.ref, pullRequest.html_url)
66687
- );
66688
- const allItems = [...workflows, ...pullRequestItems];
66689
- const status = aggregateStatus(allItems.map((item) => item.status));
66690
- const summary = summarize(status, workflows, pullRequestItems);
66691
- const envelope = {
66692
- panelId: panel.id,
66693
- state: "ok",
66694
- observedAt: (/* @__PURE__ */ new Date()).toISOString(),
66695
- link: sourceLinkForRepository(source),
66696
- signal: {
66697
- type: "pull-request-health",
66698
- status,
66699
- summary,
66700
- workflows,
66701
- pullRequests: pullRequestItems
66702
- }
66703
- };
66704
- return { envelope, response: new Response(null, { status: 200 }) };
67058
+ upstream = await githubFetch(call, { ...args, source, githubClient: githubClientFor(args) });
66705
67059
  } catch (error52) {
66706
67060
  return {
66707
67061
  response: new Response(
66708
67062
  JSON.stringify(
66709
- errorEnvelope(
66710
- panel.id,
66711
- error52 instanceof GithubAuthenticationError ? "unauthorized" : "upstream-error",
67063
+ errorEnvelope2(
67064
+ args.panel.id,
67065
+ error52 instanceof GithubAuthenticationError ? "unauthorized" : "unreachable",
66712
67066
  error52,
66713
67067
  sourceLinkForRepository(source)
66714
67068
  )
@@ -66717,34 +67071,42 @@ async function fetchGithubActionsPullRequestHealth(args) {
66717
67071
  )
66718
67072
  };
66719
67073
  }
66720
- }
66721
- async function latestRun(args) {
66722
- const call = githubRunsCall(
66723
- args.source,
66724
- args.workflow,
66725
- args.branch,
66726
- args.event,
66727
- args.branchPrefixes ? 100 : 1
66728
- );
66729
- const response = await githubFetch(call, args);
66730
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
66731
- const runs = runsSchema.parse(await response.json()).workflow_runs;
66732
- const branchPrefixes = args.branchPrefixes;
66733
- if (!branchPrefixes) return runs[0];
66734
- return runs.find((run) => {
66735
- const headBranch = run.head_branch;
66736
- return headBranch !== void 0 && headBranch !== null && branchPrefixes.some((prefix) => headBranch.startsWith(prefix));
66737
- });
66738
- }
66739
- async function openPullRequests(args) {
66740
- const result = [];
66741
- for (let page = 1; ; page += 1) {
66742
- const call = pullRequestsCall(args.source, args.baseBranch, page);
66743
- const response = await githubFetch(call, args);
66744
- if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
66745
- const pageResults = pullRequestsSchema.parse(await response.json());
66746
- result.push(...pageResults);
66747
- if (pageResults.length < 100) return result;
67074
+ if (upstream.status === 304) return { response: upstream };
67075
+ if (!upstream.ok)
67076
+ return {
67077
+ response: new Response(
67078
+ JSON.stringify(
67079
+ errorEnvelope2(
67080
+ args.panel.id,
67081
+ errorKind2(upstream.status),
67082
+ `${upstream.status} ${upstream.statusText}`,
67083
+ sourceLinkForRepository(source),
67084
+ observedAt3(upstream.headers.get("date"))
67085
+ )
67086
+ ),
67087
+ { status: 200 }
67088
+ )
67089
+ };
67090
+ try {
67091
+ return {
67092
+ envelope: normalize(await upstream.json(), observedAt3(upstream.headers.get("date"))),
67093
+ response: upstream
67094
+ };
67095
+ } catch (error52) {
67096
+ return {
67097
+ response: new Response(
67098
+ JSON.stringify(
67099
+ errorEnvelope2(
67100
+ args.panel.id,
67101
+ "upstream-error",
67102
+ error52,
67103
+ sourceLinkForRepository(source),
67104
+ observedAt3(upstream.headers.get("date"))
67105
+ )
67106
+ ),
67107
+ { status: 200 }
67108
+ )
67109
+ };
66748
67110
  }
66749
67111
  }
66750
67112
  function pullRequestsCall(source, baseBranch, page) {
@@ -66767,23 +67129,11 @@ function githubRunsCall(source, workflow, branch, event, perPage = 1) {
66767
67129
  function healthItem(label, run, detail, link) {
66768
67130
  return {
66769
67131
  label,
66770
- status: run ? normalizeStatus(run.status, run.conclusion) : "unknown",
67132
+ status: run ? normalizeStatus2(run.status, run.conclusion) : "unknown",
66771
67133
  detail: run ? `${detail} \xB7 ${run.conclusion ?? run.status}` : `${detail} \xB7 No run found`,
66772
67134
  link: link ?? run?.html_url ?? null
66773
67135
  };
66774
67136
  }
66775
- function aggregateStatus(statuses) {
66776
- if (statuses.some((status) => status === "failed")) return "failed";
66777
- if (statuses.some((status) => status === "running")) return "running";
66778
- if (statuses.some((status) => status === "unknown")) return "unknown";
66779
- if (statuses.some((status) => status === "cancelled")) return "cancelled";
66780
- return "passed";
66781
- }
66782
- function summarize(status, workflows, pullRequests) {
66783
- const failed = [...workflows, ...pullRequests].find((item) => item.status === status);
66784
- if (status !== "passed" && failed) return `${failed.label}: ${failed.detail}`;
66785
- return pullRequests.length === 0 ? `${workflows.length} update workflow${workflows.length === 1 ? "" : "s"} \xB7 No open update PRs` : `${workflows.length} update workflow${workflows.length === 1 ? "" : "s"} \xB7 ${pullRequests.length} open update PR${pullRequests.length === 1 ? "" : "s"}`;
66786
- }
66787
67137
  function sourceLinkForRepository(source) {
66788
67138
  return `https://github.com/${source.repo}`;
66789
67139
  }
@@ -66803,11 +67153,11 @@ async function fetchGithubActionsPipeline(args) {
66803
67153
  return {
66804
67154
  response: new Response(
66805
67155
  JSON.stringify(
66806
- errorEnvelope(
67156
+ errorEnvelope2(
66807
67157
  args.panel.id,
66808
67158
  error52 instanceof GithubAuthenticationError ? "unauthorized" : "unreachable",
66809
67159
  error52,
66810
- sourceLink(args.panel, args.source)
67160
+ sourceLink2(args.panel, args.source)
66811
67161
  )
66812
67162
  ),
66813
67163
  { status: 200 }
@@ -66819,12 +67169,12 @@ async function fetchGithubActionsPipeline(args) {
66819
67169
  return {
66820
67170
  response: new Response(
66821
67171
  JSON.stringify(
66822
- errorEnvelope(
67172
+ errorEnvelope2(
66823
67173
  args.panel.id,
66824
- errorKind(upstream.status),
67174
+ errorKind2(upstream.status),
66825
67175
  `${upstream.status} ${upstream.statusText}`,
66826
- sourceLink(args.panel, args.source),
66827
- observedAt(upstream.headers.get("date"))
67176
+ sourceLink2(args.panel, args.source),
67177
+ observedAt3(upstream.headers.get("date"))
66828
67178
  )
66829
67179
  ),
66830
67180
  { status: 200 }
@@ -66838,12 +67188,12 @@ async function fetchGithubActionsPipeline(args) {
66838
67188
  return {
66839
67189
  response: new Response(
66840
67190
  JSON.stringify(
66841
- errorEnvelope(
67191
+ errorEnvelope2(
66842
67192
  args.panel.id,
66843
67193
  "no-runs",
66844
67194
  parsedSource.branch ? `No workflow runs found for branch "${parsedSource.branch}". Check the source's branch setting.` : "No workflow runs found.",
66845
- sourceLink(args.panel, args.source),
66846
- observedAt(upstream.headers.get("date"))
67195
+ sourceLink2(args.panel, args.source),
67196
+ observedAt3(upstream.headers.get("date"))
66847
67197
  )
66848
67198
  ),
66849
67199
  { status: 200 }
@@ -66852,12 +67202,12 @@ async function fetchGithubActionsPipeline(args) {
66852
67202
  }
66853
67203
  const signal = {
66854
67204
  type: "pipeline-status",
66855
- status: normalizeStatus(run.status, run.conclusion),
67205
+ status: normalizeStatus2(run.status, run.conclusion),
66856
67206
  rawStatus: run.conclusion ?? run.status,
66857
67207
  name: run.name,
66858
67208
  ...run.head_branch ?? parsedSource.branch ? { branch: run.head_branch ?? parsedSource.branch } : {},
66859
- ...timestamp(run.updated_at) ? { sourceUpdatedAt: timestamp(run.updated_at) } : {},
66860
- ...timestamp(run.run_started_at) ? { runStartedAt: timestamp(run.run_started_at) } : {},
67209
+ ...timestamp2(run.updated_at) ? { sourceUpdatedAt: timestamp2(run.updated_at) } : {},
67210
+ ...timestamp2(run.run_started_at) ? { runStartedAt: timestamp2(run.run_started_at) } : {},
66861
67211
  ...run.id !== void 0 ? { sourceRunId: String(run.id) } : {},
66862
67212
  ...run.status === "completed" ? completedRunDuration(run) : {}
66863
67213
  };
@@ -66874,7 +67224,7 @@ async function fetchGithubActionsPipeline(args) {
66874
67224
  const envelope = {
66875
67225
  panelId: args.panel.id,
66876
67226
  state: "ok",
66877
- observedAt: observedAt(upstream.headers.get("date")),
67227
+ observedAt: observedAt3(upstream.headers.get("date")),
66878
67228
  link: run.html_url,
66879
67229
  signal
66880
67230
  };
@@ -66883,12 +67233,12 @@ async function fetchGithubActionsPipeline(args) {
66883
67233
  return {
66884
67234
  response: new Response(
66885
67235
  JSON.stringify(
66886
- errorEnvelope(
67236
+ errorEnvelope2(
66887
67237
  args.panel.id,
66888
67238
  "upstream-error",
66889
67239
  error52,
66890
- sourceLink(args.panel, args.source),
66891
- observedAt(upstream.headers.get("date"))
67240
+ sourceLink2(args.panel, args.source),
67241
+ observedAt3(upstream.headers.get("date"))
66892
67242
  )
66893
67243
  ),
66894
67244
  { status: 200 }
@@ -66927,45 +67277,36 @@ function githubJobsCall(source, runId) {
66927
67277
  return { url: url2.toString(), headers: new Headers() };
66928
67278
  }
66929
67279
  function completedRunDuration(run) {
66930
- const startedAt = timestamp(run.run_started_at);
66931
- const updatedAt = timestamp(run.updated_at);
67280
+ const startedAt = timestamp2(run.run_started_at);
67281
+ const updatedAt = timestamp2(run.updated_at);
66932
67282
  if (!startedAt || !updatedAt) return {};
66933
67283
  const durationMs = new Date(updatedAt).valueOf() - new Date(startedAt).valueOf();
66934
67284
  return Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {};
66935
67285
  }
66936
- function timestamp(value) {
67286
+ function timestamp2(value) {
66937
67287
  if (!value || !external_exports.iso.datetime().safeParse(value).success) return void 0;
66938
67288
  return Number.isFinite(new Date(value).valueOf()) ? value : void 0;
66939
67289
  }
66940
- function normalizeStatus(status, conclusion) {
67290
+ function normalizeStatus2(status, conclusion) {
66941
67291
  if (status !== "completed") return "running";
66942
67292
  if (conclusion === "success") return "passed";
66943
67293
  if (conclusion === "failure" || conclusion === "timed_out") return "failed";
66944
67294
  if (conclusion === "cancelled") return "cancelled";
66945
67295
  return "unknown";
66946
67296
  }
66947
- function errorKind(status) {
66948
- if (status === 401 || status === 403) return "unauthorized";
66949
- if (status === 404) return "not-found";
66950
- return "upstream-error";
67297
+ function errorKind2(status) {
67298
+ return upstreamErrorKind(status);
66951
67299
  }
66952
- function errorEnvelope(panelId, kind, error52, link, observedAt3 = (/* @__PURE__ */ new Date()).toISOString()) {
66953
- return {
66954
- panelId,
66955
- state: "error",
66956
- observedAt: observedAt3,
66957
- link,
66958
- error: { kind, message: error52 instanceof Error ? error52.message : String(error52) }
66959
- };
67300
+ function errorEnvelope2(panelId, kind, error52, link, observedAt4 = (/* @__PURE__ */ new Date()).toISOString()) {
67301
+ return upstreamErrorEnvelope({ panelId, kind, error: error52, link, upstreamDate: observedAt4 });
66960
67302
  }
66961
- function sourceLink(panel, source) {
66962
- const parsedPanel = pipelinePanelSchema.parse(panel);
67303
+ function sourceLink2(panel, source) {
67304
+ const parsedPanel = pipelinePanelSchema2.parse(panel);
66963
67305
  const parsedSource = githubActionsSourceSchema.parse(source);
66964
67306
  return `https://github.com/${parsedSource.repo}/actions/workflows/${encodeURIComponent(parsedPanel.pipeline)}`;
66965
67307
  }
66966
- function observedAt(value) {
66967
- const parsed = value ? new Date(value) : void 0;
66968
- return parsed && !Number.isNaN(parsed.valueOf()) ? parsed.toISOString() : (/* @__PURE__ */ new Date()).toISOString();
67308
+ function observedAt3(value) {
67309
+ return observedAt(value);
66969
67310
  }
66970
67311
 
66971
67312
  // packages/server/src/adapters/http-value.ts
@@ -66988,22 +67329,21 @@ async function fetchHttpValue(args) {
66988
67329
  const panel = httpValuePanelSchema.parse(args.panel);
66989
67330
  const [call] = permittedHttpValueCalls(args.panel);
66990
67331
  if (!call) throw new Error("http-value adapter declared no permitted call");
66991
- for (const name of ["if-none-match", "if-modified-since"]) {
66992
- const value = args.requestHeaders.get(name);
66993
- if (value) call.headers.set(name, value);
66994
- }
67332
+ forwardValidators(args.requestHeaders, call.headers);
66995
67333
  let upstream;
66996
67334
  try {
66997
67335
  upstream = await args.fetcher(call.url, { headers: call.headers });
66998
67336
  } catch (error52) {
66999
- return { response: errorResponse(errorEnvelope2(panel.id, "unreachable", error52, panel)) };
67337
+ return { response: errorResponse2("unreachable", error52, panel) };
67000
67338
  }
67001
67339
  if (upstream.status === 304) return { response: upstream };
67002
67340
  if (!upstream.ok) {
67003
- const kind = upstream.status === 401 || upstream.status === 403 ? "unauthorized" : upstream.status === 404 ? "not-found" : "upstream-error";
67004
67341
  return {
67005
- response: errorResponse(
67006
- errorEnvelope2(panel.id, kind, `${upstream.status} ${upstream.statusText}`, panel, upstream)
67342
+ response: errorResponse2(
67343
+ upstreamErrorKind(upstream.status),
67344
+ `${upstream.status} ${upstream.statusText}`,
67345
+ panel,
67346
+ upstream
67007
67347
  )
67008
67348
  };
67009
67349
  }
@@ -67015,7 +67355,7 @@ async function fetchHttpValue(args) {
67015
67355
  envelope: {
67016
67356
  panelId: panel.id,
67017
67357
  state: "ok",
67018
- observedAt: observedAt2(upstream.headers.get("date")),
67358
+ observedAt: observedAt(upstream.headers.get("date")),
67019
67359
  link: panel.link ?? panel.url,
67020
67360
  signal
67021
67361
  },
@@ -67023,7 +67363,7 @@ async function fetchHttpValue(args) {
67023
67363
  };
67024
67364
  } catch (error52) {
67025
67365
  return {
67026
- response: errorResponse(errorEnvelope2(panel.id, "upstream-error", error52, panel, upstream))
67366
+ response: errorResponse2("upstream-error", error52, panel, upstream)
67027
67367
  };
67028
67368
  }
67029
67369
  }
@@ -67050,21 +67390,16 @@ function extractJsonPath(value, path) {
67050
67390
  }
67051
67391
  return current;
67052
67392
  }
67053
- function errorResponse(envelope) {
67054
- return new Response(JSON.stringify(envelope), { status: 200 });
67055
- }
67056
- function errorEnvelope2(panelId, kind, error52, panel, response) {
67057
- return {
67058
- panelId,
67059
- state: "error",
67060
- observedAt: observedAt2(response?.headers.get("date") ?? null),
67061
- link: panel.link ?? panel.url,
67062
- error: { kind, message: error52 instanceof Error ? error52.message : String(error52) }
67063
- };
67064
- }
67065
- function observedAt2(value) {
67066
- const parsed = value ? new Date(value) : void 0;
67067
- return parsed && !Number.isNaN(parsed.valueOf()) ? parsed.toISOString() : (/* @__PURE__ */ new Date()).toISOString();
67393
+ function errorResponse2(kind, error52, panel, response) {
67394
+ return upstreamErrorResponse(
67395
+ upstreamErrorEnvelope({
67396
+ panelId: panel.id,
67397
+ kind,
67398
+ error: error52,
67399
+ link: panel.link ?? panel.url,
67400
+ upstreamDate: response?.headers.get("date")
67401
+ })
67402
+ );
67068
67403
  }
67069
67404
 
67070
67405
  // packages/server/src/allowlist.ts
@@ -67074,17 +67409,26 @@ function deriveAllowlist(config2) {
67074
67409
  for (const panel of board.panels) {
67075
67410
  const source = panel.source ? config2.sources[panel.source] : void 0;
67076
67411
  const calls = callsFor(panel, source);
67077
- if (calls) allowed.set(`${boardName}/${panel.id}`, calls);
67412
+ if (calls) allowed.set(`${boardName}/${panel.id}`, /* @__PURE__ */ new Set(["read"]));
67413
+ if (panel.type === "pull-request-health" && source?.type === "github-actions") {
67414
+ allowed.set(
67415
+ `${boardName}/${panel.id}`,
67416
+ /* @__PURE__ */ new Set(["pull-requests", "update-workflow", "pull-request-build"])
67417
+ );
67418
+ }
67078
67419
  }
67079
67420
  }
67080
67421
  return allowed;
67081
67422
  }
67423
+ function permitsPanelOperation(allowlist, board, panel, operation2) {
67424
+ return allowlist.get(`${board}/${panel}`)?.has(operation2) ?? false;
67425
+ }
67082
67426
  function callsFor(panel, source) {
67083
67427
  if (panel.type === "pipeline-status" && source?.type === "github-actions") {
67084
67428
  return permittedGithubActionsCalls(panel, source);
67085
67429
  }
67086
- if (panel.type === "pull-request-health" && source?.type === "github-actions") {
67087
- return permittedGithubActionsPullRequestHealthCalls(panel, source);
67430
+ if (panel.type === "pipeline-status" && source?.type === "azure-devops") {
67431
+ return permittedAzureDevOpsCalls(panel, source);
67088
67432
  }
67089
67433
  if (panel.type === "http-value") return permittedHttpValueCalls(panel);
67090
67434
  return void 0;
@@ -67169,7 +67513,6 @@ function serializeForScriptTag(value) {
67169
67513
  function createApp(deps) {
67170
67514
  const { config: config2 } = deps;
67171
67515
  const templates = new TemplateCache(deps.fetcher ?? globalThis.fetch);
67172
- const clientVersion = deps.clientVersion ?? deriveVersionLabel(config2.assetPath);
67173
67516
  const selectedBoard = config2.board ?? Object.keys(deps.boardConfig?.boards ?? {})[0] ?? "ze-great-team";
67174
67517
  const allowlist = deps.boardConfig ? deriveAllowlist(deps.boardConfig) : /* @__PURE__ */ new Map();
67175
67518
  const credentials = deps.credentials ?? environmentCredentials();
@@ -67178,7 +67521,7 @@ function createApp(deps) {
67178
67521
  app.get("/health", (c5) => c5.json({ status: "ok" }));
67179
67522
  app.use("/api/*", async (_c5, next) => next());
67180
67523
  app.get(`${config2.proxyPath}/client`, (c5) => {
67181
- const identity = { assetPath: config2.assetPath, clientVersion };
67524
+ const identity = { assetPath: config2.assetPath };
67182
67525
  return c5.json(identity, 200, { "cache-control": "no-store" });
67183
67526
  });
67184
67527
  app.get("/", (c5) => renderEntrypoint(c5.req.raw, selectedBoard));
@@ -67236,7 +67579,7 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
67236
67579
  const board = deps.boardConfig?.boards[boardName];
67237
67580
  const panel = board?.panels.find((candidate) => candidate.id === panelId);
67238
67581
  const source = panel?.source ? deps.boardConfig?.sources[panel.source] : void 0;
67239
- if (!panel || panel.type !== "http-value" && !source || !allowlist.has(`${boardName}/${panelId}`))
67582
+ if (!panel || panel.type !== "http-value" && !source || !permitsPanelOperation(allowlist, boardName, panelId, "read"))
67240
67583
  return c5.notFound();
67241
67584
  if (panel.type === "pipeline-status" && source?.type === "github-actions" && source) {
67242
67585
  const result = await fetchGithubActionsPipeline({
@@ -67246,46 +67589,93 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
67246
67589
  fetcher: deps.fetcher ?? globalThis.fetch,
67247
67590
  githubClient
67248
67591
  });
67249
- const headers = passthroughHeaders(result.response.headers);
67250
- if (result.response.status === 304) return new Response(null, { status: 304, headers });
67251
- const envelope = result.envelope ?? JSON.parse(await result.response.text());
67252
- headers.set("content-type", "application/json; charset=utf-8");
67253
- return new Response(JSON.stringify(envelope), { status: 200, headers });
67254
- }
67255
- if (panel.type === "pull-request-health" && source?.type === "github-actions" && source) {
67256
- const result = await fetchGithubActionsPullRequestHealth({
67592
+ return adapterRouteResponse(result);
67593
+ }
67594
+ if (panel.type === "pipeline-status" && source?.type === "azure-devops" && source) {
67595
+ const result = await fetchAzureDevOpsPipeline({
67257
67596
  panel,
67258
67597
  source,
67259
67598
  requestHeaders: c5.req.raw.headers,
67260
67599
  fetcher: deps.fetcher ?? globalThis.fetch,
67261
- githubClient
67600
+ credentials
67262
67601
  });
67263
- const headers = passthroughHeaders(result.response.headers);
67264
- const envelope = result.envelope ?? JSON.parse(await result.response.text());
67265
- headers.set("content-type", "application/json; charset=utf-8");
67266
- return new Response(JSON.stringify(envelope), { status: 200, headers });
67602
+ return adapterRouteResponse(result);
67267
67603
  }
67604
+ if (panel.type === "pull-request-health") return c5.notFound();
67268
67605
  if (panel.type === "http-value") {
67269
67606
  const result = await fetchHttpValue({
67270
67607
  panel,
67271
67608
  requestHeaders: c5.req.raw.headers,
67272
67609
  fetcher: deps.fetcher ?? globalThis.fetch
67273
67610
  });
67274
- const headers = passthroughHeaders(result.response.headers);
67275
- if (result.response.status === 304) return new Response(null, { status: 304, headers });
67276
- const envelope = result.envelope ?? JSON.parse(await result.response.text());
67277
- headers.set("content-type", "application/json; charset=utf-8");
67278
- return new Response(JSON.stringify(envelope), { status: 200, headers });
67611
+ return adapterRouteResponse(result);
67279
67612
  }
67280
67613
  return c5.notFound();
67281
67614
  });
67615
+ app.get("/api/panel/:board/:panelId/pull-requests", async (c5) => {
67616
+ const resolved = pullRequestPanel(c5.req.param("board"), c5.req.param("panelId"), "pull-requests");
67617
+ if (!resolved) return c5.notFound();
67618
+ return adapterRouteResponse(
67619
+ await fetchGithubActionsPullRequestCandidates({
67620
+ ...resolved,
67621
+ requestHeaders: c5.req.raw.headers,
67622
+ fetcher: deps.fetcher ?? globalThis.fetch,
67623
+ githubClient
67624
+ })
67625
+ );
67626
+ });
67627
+ app.get("/api/panel/:board/:panelId/update-workflow/:workflow", async (c5) => {
67628
+ const resolved = pullRequestPanel(
67629
+ c5.req.param("board"),
67630
+ c5.req.param("panelId"),
67631
+ "update-workflow"
67632
+ );
67633
+ const workflow = c5.req.param("workflow");
67634
+ if (!resolved?.capabilities.configuredUpdateWorkflow(workflow)) return c5.notFound();
67635
+ return adapterRouteResponse(
67636
+ await fetchGithubActionsUpdateWorkflow({
67637
+ ...resolved,
67638
+ workflow,
67639
+ requestHeaders: c5.req.raw.headers,
67640
+ fetcher: deps.fetcher ?? globalThis.fetch,
67641
+ githubClient
67642
+ })
67643
+ );
67644
+ });
67645
+ app.get("/api/panel/:board/:panelId/pull-request-build", async (c5) => {
67646
+ const resolved = pullRequestPanel(
67647
+ c5.req.param("board"),
67648
+ c5.req.param("panelId"),
67649
+ "pull-request-build"
67650
+ );
67651
+ const branch = c5.req.query("branch");
67652
+ if (!resolved || !branch || !resolved.capabilities.permitsBuildBranch(branch))
67653
+ return c5.notFound();
67654
+ return adapterRouteResponse(
67655
+ await fetchGithubActionsPullRequestBuild({
67656
+ ...resolved,
67657
+ branch,
67658
+ requestHeaders: c5.req.raw.headers,
67659
+ fetcher: deps.fetcher ?? globalThis.fetch,
67660
+ githubClient
67661
+ })
67662
+ );
67663
+ });
67664
+ function pullRequestPanel(boardName, panelId, operation2) {
67665
+ const panel = deps.boardConfig?.boards[boardName]?.panels.find(
67666
+ (candidate) => candidate.id === panelId
67667
+ );
67668
+ const source = panel?.source ? deps.boardConfig?.sources[panel.source] : void 0;
67669
+ const capabilities = panel?.type === "pull-request-health" ? pullRequestHealthCapabilities(panel) : void 0;
67670
+ if (!capabilities) return void 0;
67671
+ return panel?.type === "pull-request-health" && source?.type === "github-actions" && permitsPanelOperation(allowlist, boardName, panelId, operation2) ? { panel, source, capabilities } : void 0;
67672
+ }
67282
67673
  async function renderEntrypoint(_request, board) {
67283
67674
  const template = await templates.get(config2.assetPath);
67284
67675
  const env2 = {
67285
67676
  assetPath: config2.assetPath,
67286
67677
  proxyPath: config2.proxyPath,
67287
- board,
67288
- clientVersion
67678
+ board
67289
67679
  };
67290
67680
  return new Response(renderIndexHtml(template, env2), {
67291
67681
  headers: {
@@ -67302,20 +67692,6 @@ function safeDownloadFilename(value) {
67302
67692
  const sanitized = value.replace(/[\\/\r\n"%*:|<>?]/g, "_").trim();
67303
67693
  return sanitized || "board";
67304
67694
  }
67305
- function passthroughHeaders(upstream) {
67306
- const headers = new Headers();
67307
- for (const name of ["cache-control", "etag", "last-modified", "date", "vary"]) {
67308
- const value = upstream.get(name);
67309
- if (value) headers.set(name, value);
67310
- }
67311
- return headers;
67312
- }
67313
- function deriveVersionLabel(assetPath) {
67314
- const segments = assetPath.split("/").filter((segment) => segment !== "");
67315
- const last = segments.at(-1);
67316
- if (last === void 0) return "unknown";
67317
- return last === ASSET_PATH_SENTINEL.replace("/", "") ? "dev" : last;
67318
- }
67319
67695
 
67320
67696
  // packages/server/src/board-config.ts
67321
67697
  import { readFile as readFile3 } from "node:fs/promises";