@usepipr/runtime 0.4.2 → 0.4.3

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.
@@ -1,5 +1,5 @@
1
1
  import { c as resolveReadAtRefRequest, d as isRecord, l as unavailableReadAtRefResult, n as boundedLineSlice, r as parseManifestPath, u as createDiffRangeIndex } from "./runtime-tools-core-D8_WsbL_.mjs";
2
- import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-BuZs6bOx.mjs";
2
+ import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-Bjlu-FL3.mjs";
3
3
  import { t as isPublishableSuggestedFixSelection } from "./suggested-fix-publication-policy-B5Wwuudp.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { chmod, chown, cp, lstat, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
@@ -597,7 +597,7 @@ function compareStableSemver(left, right) {
597
597
  }
598
598
  //#endregion
599
599
  //#region src/shared/version.ts
600
- const runtimeVersion = "0.4.2";
600
+ const runtimeVersion = "0.4.3";
601
601
  //#endregion
602
602
  //#region src/config/version-compat.ts
603
603
  async function resolveConfigVersionCompatibility(options) {
@@ -928,10 +928,6 @@ function assertRequiredProviderEnv(providers, options) {
928
928
  if (missing.length > 0) throw new Error(`Missing provider env vars: ${missing.map((provider) => provider.apiKeyEnv).join(", ")}`);
929
929
  }
930
930
  //#endregion
931
- //#region src/config/scaffold-versions.ts
932
- const defaultTypesBunVersion = "1.3.14";
933
- const defaultTypescriptVersion = "6.0.3";
934
- //#endregion
935
931
  //#region src/config/init.ts
936
932
  const supportedOfficialInitAdapters = [
937
933
  "github",
@@ -939,8 +935,8 @@ const supportedOfficialInitAdapters = [
939
935
  "azure-devops",
940
936
  "bitbucket"
941
937
  ];
942
- const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.2";
943
- const defaultSdkVersion = "0.4.2";
938
+ const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.3";
939
+ const defaultSdkVersion = "0.4.3";
944
940
  function resolveOfficialInitAdapters(adapters) {
945
941
  if (adapters === void 0) return ["github"];
946
942
  if (adapters.length === 0) return [];
@@ -974,11 +970,7 @@ async function initOfficialMinimalProject(options) {
974
970
  const result = await writeTargets(targets, existing, { skipExisting: false });
975
971
  if (!minimal) {
976
972
  await assertBunAvailable();
977
- const install = Bun.spawn([
978
- "bun",
979
- "install",
980
- "--ignore-scripts"
981
- ], {
973
+ const install = Bun.spawn(initInstallCommand(), {
982
974
  cwd: projectDir,
983
975
  env: process.env,
984
976
  stdout: "pipe",
@@ -1000,6 +992,15 @@ async function initOfficialMinimalProject(options) {
1000
992
  ...result
1001
993
  };
1002
994
  }
995
+ function initInstallCommand(env = process.env) {
996
+ const command = [
997
+ "bun",
998
+ "install",
999
+ "--ignore-scripts"
1000
+ ];
1001
+ if (env.PIPR_INTERNAL_INIT_OFFLINE === "1") command.push("--offline");
1002
+ return command;
1003
+ }
1003
1004
  async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false) {
1004
1005
  const files = [{
1005
1006
  relativePath: path.join(relativeConfigDir, "config.ts"),
@@ -1086,15 +1087,18 @@ function starterBitbucketWebhookEnvironment(recipe) {
1086
1087
  lines.push("");
1087
1088
  return lines.join("\n");
1088
1089
  }
1089
- function starterPackageJson() {
1090
- return `${JSON.stringify({
1090
+ function officialInitPackageManifest(env = process.env) {
1091
+ return {
1091
1092
  private: true,
1092
- dependencies: { "@usepipr/sdk": process.env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
1093
+ dependencies: { "@usepipr/sdk": env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
1093
1094
  devDependencies: {
1094
- "@types/bun": defaultTypesBunVersion,
1095
- typescript: defaultTypescriptVersion
1095
+ "@types/bun": env.PIPR_INTERNAL_INIT_TYPES_BUN_VERSION ?? "1.3.14",
1096
+ typescript: env.PIPR_INTERNAL_INIT_TYPESCRIPT_VERSION ?? "6.0.3"
1096
1097
  }
1097
- }, null, 2)}\n`;
1098
+ };
1099
+ }
1100
+ function starterPackageJson() {
1101
+ return `${JSON.stringify(officialInitPackageManifest(), null, 2)}\n`;
1098
1102
  }
1099
1103
  async function writeTargets(targets, existing, options) {
1100
1104
  const created = [];
@@ -1150,996 +1154,172 @@ async function maybeLstat(filePath) {
1150
1154
  }
1151
1155
  }
1152
1156
  //#endregion
1153
- //#region src/hosts/http.ts
1154
- var CodeHostHttpError = class extends Error {
1155
- status;
1156
- constructor(message, status) {
1157
- super(message);
1158
- this.name = "CodeHostHttpError";
1159
- this.status = status;
1157
+ //#region src/diff/git.ts
1158
+ function runGit$1(args, cwd, maxBuffer) {
1159
+ const result = Bun.spawnSync(["git", ...args], {
1160
+ cwd,
1161
+ env: process.env,
1162
+ maxBuffer,
1163
+ stderr: "pipe",
1164
+ stdout: "pipe"
1165
+ });
1166
+ if (result.exitCode !== 0) {
1167
+ const failure = result.stderr?.toString().trim() || "unknown error";
1168
+ throw new Error(`git ${args.join(" ")} failed: ${failure}`);
1160
1169
  }
1161
- };
1162
- function createCodeHostHttpClient(options) {
1163
- const fetchRequest = options.fetch ?? fetch;
1164
- const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
1165
- const successThrottle = options.successThrottle ?? createCodeHostSuccessThrottle(sleep);
1166
- const maxRetries = options.maxRetries ?? 2;
1167
- const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
1168
- const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
1169
- return { async json(path, schema, init = {}) {
1170
- const method = init.method?.toUpperCase() ?? "GET";
1171
- for (let attempt = 0;; attempt += 1) {
1172
- await successThrottle.wait();
1173
- const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
1174
- const response = await fetchRequest(new URL(path, options.baseUrl), {
1175
- ...init,
1176
- headers: {
1177
- ...Object.fromEntries(new Headers(options.headers)),
1178
- ...Object.fromEntries(new Headers(init.headers))
1179
- },
1180
- signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
1181
- });
1182
- const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
1183
- if (response.ok) {
1184
- successThrottle.update(response.headers.get("Retry-After"));
1185
- return schema.parse(await response.json());
1186
- }
1187
- if (shouldRetryCodeHostRequest({
1188
- method,
1189
- attempt,
1190
- maxRetries,
1191
- response,
1192
- retryStatuses: options.retryNonIdempotentStatuses
1193
- })) {
1194
- await sleep(retryAfter || 250 * 2 ** attempt);
1195
- continue;
1196
- }
1197
- const body = (await response.text()).slice(0, 1024);
1198
- throw new CodeHostHttpError(redact$1(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
1170
+ return result.stdout.toString();
1171
+ }
1172
+ //#endregion
1173
+ //#region src/diff/diff.ts
1174
+ const lockFilePattern = /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock)$/;
1175
+ const generatedPattern = /(^|\/)(dist|build|coverage|vendor)\//;
1176
+ const maxInlineChangedLines = 1e3;
1177
+ const maxCommentableRangeLines = 5e3;
1178
+ function buildDiffManifest(options) {
1179
+ const mergeBaseSha = runGit$1([
1180
+ "merge-base",
1181
+ options.baseSha,
1182
+ options.headSha
1183
+ ], options.cwd).trim();
1184
+ const diffHead = options.includeWorkingTree ? void 0 : options.headSha;
1185
+ const files = parseNameStatus(runGit$1(buildDiffArgs([
1186
+ "--name-status",
1187
+ "-z",
1188
+ "--find-renames"
1189
+ ], mergeBaseSha, diffHead), options.cwd));
1190
+ const diffStats = getDiffStats(options.cwd, mergeBaseSha, diffHead);
1191
+ const rawPatch = parseRawPatch(runGit$1(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(files, diffStats)), options.cwd));
1192
+ const parsedDiff = parseUnifiedDiff(rawPatch.patch, rawPatch.filePaths);
1193
+ for (const file of files) {
1194
+ const stats = diffStats.get(file.path);
1195
+ file.additions = stats?.additions ?? 0;
1196
+ file.deletions = stats?.deletions ?? 0;
1197
+ const preExcludedReason = stats?.excludedReason;
1198
+ const parsedFile = parsedDiff.get(file.path);
1199
+ file.hunks = preExcludedReason ? [] : parsedFile?.hunks ?? [];
1200
+ file.commentableRanges = preExcludedReason ? [] : parsedFile?.commentableRanges ?? [];
1201
+ const excludedReason = preExcludedReason ?? getExcludedReason(file);
1202
+ if (excludedReason) {
1203
+ file.hunks = [];
1204
+ file.commentableRanges = [];
1205
+ file.excludedReason = excludedReason;
1199
1206
  }
1200
- } };
1207
+ }
1208
+ return parseDiffManifest({
1209
+ baseSha: options.baseSha,
1210
+ headSha: options.headSha,
1211
+ mergeBaseSha,
1212
+ files
1213
+ });
1201
1214
  }
1202
- function createCodeHostSuccessThrottle(sleep = (milliseconds) => Bun.sleep(milliseconds)) {
1203
- let notBefore = 0;
1204
- let waitInFlight;
1205
- const waitUntilReady = async () => {
1206
- while (true) {
1207
- const deadline = notBefore;
1208
- const remaining = deadline - Date.now();
1209
- if (remaining <= 0) {
1210
- if (notBefore === deadline) notBefore = 0;
1211
- return;
1212
- }
1213
- await sleep(remaining);
1214
- if (notBefore <= deadline) {
1215
- notBefore = 0;
1216
- return;
1217
- }
1215
+ function parseNameStatus(output) {
1216
+ const fields = output.split("\0");
1217
+ const files = [];
1218
+ let index = 0;
1219
+ while (index < fields.length) {
1220
+ const rawStatus = fields[index++];
1221
+ if (!rawStatus) continue;
1222
+ const firstPath = fields[index++] ?? "";
1223
+ const status = parseFileStatus(rawStatus);
1224
+ if (status === "renamed") {
1225
+ const secondPath = fields[index++] ?? "";
1226
+ files.push(baseFile(secondPath || firstPath, status, firstPath));
1227
+ continue;
1218
1228
  }
1219
- };
1220
- return {
1221
- wait() {
1222
- if (waitInFlight) return waitInFlight;
1223
- const currentWait = waitUntilReady().finally(() => {
1224
- if (waitInFlight === currentWait) waitInFlight = void 0;
1229
+ files.push(baseFile(firstPath, status));
1230
+ }
1231
+ return files;
1232
+ }
1233
+ function parseUnifiedDiff(diff, filePaths) {
1234
+ const state = createDiffParserState(filePaths);
1235
+ for (const line of diff.split("\n")) parseUnifiedDiffLine(state, line);
1236
+ finishActiveHunk(state);
1237
+ return state.filesByPath;
1238
+ }
1239
+ function getDiffStats(cwd, baseSha, headSha) {
1240
+ const output = runGit$1(buildDiffArgs([
1241
+ "--numstat",
1242
+ "-z",
1243
+ "--find-renames"
1244
+ ], baseSha, headSha), cwd);
1245
+ const stats = /* @__PURE__ */ new Map();
1246
+ for (const stat of parseNumstat(output)) {
1247
+ if (stat.binary) {
1248
+ stats.set(stat.path, {
1249
+ additions: 0,
1250
+ deletions: 0,
1251
+ excludedReason: "binary diff"
1225
1252
  });
1226
- waitInFlight = currentWait;
1227
- return waitInFlight;
1228
- },
1229
- update(retryAfter) {
1230
- const delay = retryAfterMilliseconds(retryAfter);
1231
- if (delay > 0) notBefore = Math.max(notBefore, Date.now() + delay);
1253
+ continue;
1232
1254
  }
1233
- };
1255
+ stats.set(stat.path, {
1256
+ additions: stat.additions,
1257
+ deletions: stat.deletions,
1258
+ excludedReason: stat.additions + stat.deletions > maxInlineChangedLines ? "oversized diff" : void 0
1259
+ });
1260
+ }
1261
+ return stats;
1234
1262
  }
1235
- function shouldRetryCodeHostRequest(options) {
1236
- return (options.method === "GET" || options.method === "HEAD" || options.retryStatuses?.includes(options.response.status) === true) && options.attempt < options.maxRetries && (options.response.status === 429 || options.response.status >= 500 || options.retryStatuses?.includes(options.response.status) === true);
1263
+ function getPreExcludedFiles(files, stats) {
1264
+ const excluded = /* @__PURE__ */ new Map();
1265
+ for (const [filePath, stat] of stats) if (stat.excludedReason) excluded.set(filePath, stat.excludedReason);
1266
+ for (const file of files) {
1267
+ const excludedReason = excluded.get(file.path) ?? getPathExcludedReason(file);
1268
+ if (!excludedReason) continue;
1269
+ excluded.set(file.path, excludedReason);
1270
+ if (file.previousPath) excluded.set(file.previousPath, excludedReason);
1271
+ }
1272
+ return excluded;
1237
1273
  }
1238
- function retryAfterMilliseconds(value) {
1239
- if (!value) return 0;
1240
- const seconds = Number(value);
1241
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1242
- const date = Date.parse(value);
1243
- return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
1274
+ function buildUnifiedDiffArgs(baseSha, headSha, excludedFiles) {
1275
+ const args = buildDiffArgs([
1276
+ "--raw",
1277
+ "-z",
1278
+ "--patch",
1279
+ "--submodule=short",
1280
+ "--unified=80",
1281
+ "--find-renames"
1282
+ ], baseSha, headSha);
1283
+ if (excludedFiles.size === 0) return args;
1284
+ return [
1285
+ ...args,
1286
+ "--",
1287
+ ".",
1288
+ ...[...excludedFiles.keys()].map((filePath) => `:(exclude,literal)${filePath}`)
1289
+ ];
1244
1290
  }
1245
- function redact$1(value, secrets) {
1246
- let redacted = value;
1247
- for (const secret of secrets) redacted = redacted.split(secret).join("***");
1248
- return redacted;
1291
+ function buildDiffArgs(options, baseSha, headSha) {
1292
+ return headSha ? [
1293
+ "diff",
1294
+ ...options,
1295
+ baseSha,
1296
+ headSha
1297
+ ] : [
1298
+ "diff",
1299
+ ...options,
1300
+ baseSha
1301
+ ];
1249
1302
  }
1250
- //#endregion
1251
- //#region src/hosts/azure-devops/coordinates.ts
1252
- function azureOrganizationFromUrl(value) {
1253
- const url = new URL(value);
1254
- return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
1255
- }
1256
- //#endregion
1257
- //#region src/hosts/azure-devops/client.ts
1258
- const identitySchema = z.looseObject({
1259
- displayName: z.string().optional(),
1260
- providerDisplayName: z.string().optional(),
1261
- uniqueName: z.string().optional(),
1262
- id: z.string().optional(),
1263
- properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
1264
- }).transform((identity) => {
1265
- const displayName = identity.displayName ?? identity.providerDisplayName;
1266
- const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
1267
- return {
1268
- ...identity,
1269
- ...displayName ? { displayName } : {},
1270
- ...uniqueName ? { uniqueName } : {}
1271
- };
1272
- });
1273
- const commitSchema = z.looseObject({ commitId: z.string().min(1) });
1274
- const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
1275
- const pullRequestSchema$1 = z.looseObject({
1276
- pullRequestId: z.number().int().positive(),
1277
- isDraft: z.boolean().optional(),
1278
- title: z.string(),
1279
- description: z.string().nullish(),
1280
- url: z.string().optional(),
1281
- sourceRefName: z.string().min(1),
1282
- targetRefName: z.string().min(1),
1283
- createdBy: identitySchema.optional(),
1284
- lastMergeSourceCommit: commitSchema,
1285
- lastMergeTargetCommit: commitSchema,
1286
- forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
1287
- repository: z.looseObject({
1288
- id: z.string().min(1),
1289
- name: z.string().min(1),
1290
- url: z.string().optional(),
1291
- project: z.looseObject({
1292
- id: z.string().min(1),
1293
- name: z.string().min(1)
1294
- })
1295
- })
1296
- });
1297
- const repositorySchema$1 = z.looseObject({
1298
- id: z.string().min(1),
1299
- name: z.string().min(1),
1300
- project: z.looseObject({
1301
- id: z.string().min(1),
1302
- name: z.string().min(1)
1303
- })
1304
- });
1305
- const iterationSchema = z.looseObject({
1306
- id: z.number().int().positive(),
1307
- sourceRefCommit: commitSchema
1308
- });
1309
- const iterationChangeSchema = z.looseObject({
1310
- changeTrackingId: z.number().int(),
1311
- changeType: z.string(),
1312
- item: z.looseObject({
1313
- path: z.string().min(1),
1314
- originalPath: z.string().min(1).optional()
1315
- })
1316
- });
1317
- const threadCommentSchema = z.looseObject({
1318
- id: z.union([z.number(), z.string()]).transform(String),
1319
- parentCommentId: z.number().int().optional(),
1320
- content: z.string().default(""),
1321
- author: identitySchema.optional(),
1322
- isDeleted: z.boolean().optional()
1323
- });
1324
- const pointSchema = z.looseObject({
1325
- line: z.number().int(),
1326
- offset: z.number().int()
1327
- });
1328
- const threadSchema = z.looseObject({
1329
- id: z.union([z.number(), z.string()]).transform(String),
1330
- status: z.string().optional(),
1331
- comments: z.array(threadCommentSchema).default([]),
1332
- threadContext: z.looseObject({
1333
- filePath: z.string().optional(),
1334
- leftFileStart: pointSchema.nullish(),
1335
- leftFileEnd: pointSchema.nullish(),
1336
- rightFileStart: pointSchema.nullish(),
1337
- rightFileEnd: pointSchema.nullish()
1338
- }).nullish(),
1339
- pullRequestThreadContext: z.looseObject({
1340
- changeTrackingId: z.number().int().optional(),
1341
- iterationContext: z.looseObject({
1342
- firstComparingIteration: z.number().int(),
1343
- secondComparingIteration: z.number().int()
1344
- }).optional()
1345
- }).nullish()
1346
- });
1347
- const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
1348
- const aclSchema = collectionSchema(z.looseObject({
1349
- inheritPermissions: z.boolean().default(true),
1350
- acesDictionary: z.record(z.string(), z.looseObject({
1351
- allow: z.number().int().default(0),
1352
- deny: z.number().int().default(0),
1353
- extendedInfo: z.looseObject({
1354
- effectiveAllow: z.number().int().default(0),
1355
- effectiveDeny: z.number().int().default(0)
1356
- }).optional()
1357
- }))
1358
- }));
1359
- function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch, options = {}) {
1360
- const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
1361
- const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
1362
- const pat = env.AZURE_DEVOPS_TOKEN;
1363
- const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
1364
- if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
1365
- if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
1366
- if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
1367
- const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
1368
- const successThrottle = createCodeHostSuccessThrottle(options.sleep);
1369
- const api = createCodeHostHttpClient({
1370
- baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
1371
- headers,
1372
- fetch,
1373
- successThrottle
1374
- });
1375
- const organizationApi = createCodeHostHttpClient({
1376
- baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
1377
- headers,
1378
- fetch,
1379
- successThrottle
1380
- });
1381
- const identityApi = createCodeHostHttpClient({
1382
- baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
1383
- headers,
1384
- fetch,
1385
- successThrottle
1386
- });
1387
- const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
1388
- return {
1389
- organization,
1390
- project,
1391
- async currentUser() {
1392
- return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
1393
- },
1394
- async getRepository(repository) {
1395
- const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
1396
- return {
1397
- id: value.id,
1398
- name: value.name,
1399
- projectId: value.project.id,
1400
- project: value.project.name
1401
- };
1402
- },
1403
- async getRepositoryPermission(actor, projectId, repositoryId) {
1404
- const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
1405
- descriptor: z.string().min(1),
1406
- isActive: z.boolean().optional(),
1407
- isContainer: z.boolean().optional(),
1408
- memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
1409
- })))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
1410
- if (candidates.length !== 1) return "none";
1411
- const identity = candidates[0];
1412
- if (!identity) return "none";
1413
- const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
1414
- const loadAcls = (token) => {
1415
- const query = new URLSearchParams({
1416
- token,
1417
- descriptors: descriptors.join(","),
1418
- includeExtendedInfo: "true",
1419
- recurse: "false",
1420
- "api-version": "7.1"
1421
- });
1422
- return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
1423
- };
1424
- const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
1425
- return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
1426
- },
1427
- getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
1428
- async listIterations(repositoryId, changeNumber) {
1429
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
1430
- id: iteration.id,
1431
- headSha: iteration.sourceRefCommit.commitId
1432
- }));
1433
- },
1434
- async loadChange(options) {
1435
- if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
1436
- const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
1437
- const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
1438
- const headSha = pullRequest.lastMergeSourceCommit.commitId;
1439
- const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
1440
- if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
1441
- const sourceRef = branchName(pullRequest.sourceRefName);
1442
- const targetRef = branchName(pullRequest.targetRefName);
1443
- return {
1444
- repository: {
1445
- slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
1446
- url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
1447
- },
1448
- coordinates: {
1449
- provider: "azure-devops",
1450
- organization,
1451
- project: pullRequest.repository.project.name,
1452
- projectId: pullRequest.repository.project.id,
1453
- repositoryId: pullRequest.repository.id
1454
- },
1455
- change: {
1456
- number: pullRequest.pullRequestId,
1457
- isDraft: pullRequest.isDraft,
1458
- title: pullRequest.title,
1459
- description: pullRequest.description ?? "",
1460
- url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
1461
- author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
1462
- base: {
1463
- sha: pullRequest.lastMergeTargetCommit.commitId,
1464
- ref: targetRef
1465
- },
1466
- head: {
1467
- sha: headSha,
1468
- ref: sourceRef,
1469
- ...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
1470
- },
1471
- ...pullRequest.forkSource ? { isFork: true } : {}
1472
- },
1473
- iterationId: iteration.id
1474
- };
1475
- },
1476
- async listIterationChanges(repositoryId, changeNumber, iterationId) {
1477
- const changes = [];
1478
- let skip = 0;
1479
- let top = 2e3;
1480
- for (;;) {
1481
- const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
1482
- const page = await api.json(withApiVersion(path), z.looseObject({
1483
- changeEntries: z.array(iterationChangeSchema),
1484
- nextSkip: z.number().int().nonnegative().optional(),
1485
- nextTop: z.number().int().nonnegative().max(2e3).optional()
1486
- }));
1487
- changes.push(...page.changeEntries.map((change) => ({
1488
- changeTrackingId: change.changeTrackingId,
1489
- changeType: change.changeType,
1490
- path: trimLeadingSlash(change.item.path),
1491
- ...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
1492
- })));
1493
- if (!page.nextSkip) return changes;
1494
- skip = page.nextSkip;
1495
- if (page.nextTop) top = page.nextTop;
1496
- }
1497
- },
1498
- async listThreads(repositoryId, changeNumber) {
1499
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
1500
- },
1501
- createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
1502
- updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
1503
- createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
1504
- updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
1505
- async createStatus(repositoryId, changeNumber, body) {
1506
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
1507
- }
1508
- };
1509
- }
1510
- function azureDevOpsStatusState(state) {
1511
- switch (state) {
1512
- case "pending": return "pending";
1513
- case "success": return "succeeded";
1514
- case "failure": return "failed";
1515
- case "neutral": return "notApplicable";
1516
- }
1517
- }
1518
- function azureRepositoryPermission(bits) {
1519
- if ((bits & 2) === 0) return "none";
1520
- if ((bits & 8192) !== 0) return "admin";
1521
- if ((bits & 2048) !== 0) return "maintain";
1522
- if ((bits & 4) !== 0) return "write";
1523
- if ((bits & 16384) !== 0) return "triage";
1524
- return "read";
1525
- }
1526
- function azurePermissionFromAcls(acls) {
1527
- let allow = 0;
1528
- let deny = 0;
1529
- for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
1530
- allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
1531
- deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
1532
- }
1533
- return azureRepositoryPermission(allow & ~deny);
1534
- }
1535
- function collectionSchema(item) {
1536
- return z.looseObject({
1537
- count: z.number().int().nonnegative(),
1538
- value: z.array(item)
1539
- });
1540
- }
1541
- function withApiVersion(path) {
1542
- return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
1543
- }
1544
- function jsonRequest$2(method, body) {
1545
- return {
1546
- method,
1547
- headers: { "Content-Type": "application/json" },
1548
- body: JSON.stringify(body)
1549
- };
1550
- }
1551
- function organizationFromCollectionUri$1(value) {
1552
- return value ? azureOrganizationFromUrl(value) : void 0;
1553
- }
1554
- function branchName(ref) {
1555
- return ref.replace(/^refs\/heads\//, "");
1556
- }
1557
- function repositoryWebUrl(organization, project, repository) {
1558
- return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
1559
- }
1560
- function trimLeadingSlash(value) {
1561
- return value.replace(/^\/+/, "");
1562
- }
1563
- //#endregion
1564
- //#region src/hosts/bitbucket/schema.ts
1565
- const bitbucketRepositorySchema = z.looseObject({
1566
- uuid: z.string().min(1),
1567
- name: z.string().min(1),
1568
- full_name: z.string().min(1),
1569
- slug: z.string().min(1).optional(),
1570
- links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
1571
- }).transform((repository) => ({
1572
- ...repository,
1573
- slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
1574
- }));
1575
- //#endregion
1576
- //#region src/hosts/bitbucket/client.ts
1577
- const userSchema$1 = z.looseObject({
1578
- uuid: z.string().optional(),
1579
- nickname: z.string().optional(),
1580
- display_name: z.string().optional()
1581
- });
1582
- const endpointSchema = z.looseObject({
1583
- branch: z.looseObject({ name: z.string().min(1) }),
1584
- commit: z.looseObject({ hash: z.string().min(1) }),
1585
- repository: bitbucketRepositorySchema
1586
- });
1587
- const pullRequestSchema = z.looseObject({
1588
- id: z.number().int().positive(),
1589
- draft: z.boolean().optional(),
1590
- title: z.string(),
1591
- description: z.string().default(""),
1592
- author: userSchema$1.optional(),
1593
- source: endpointSchema,
1594
- destination: endpointSchema,
1595
- links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
1596
- });
1597
- const inlineSchema = z.looseObject({
1598
- path: z.string().optional(),
1599
- from: z.number().int().nullable().optional(),
1600
- to: z.number().int().nullable().optional(),
1601
- start_from: z.number().int().nullable().optional(),
1602
- start_to: z.number().int().nullable().optional()
1603
- });
1604
- const commentSchema = z.looseObject({
1605
- id: z.union([z.number(), z.string()]).transform(String),
1606
- content: z.looseObject({ raw: z.string().default("") }),
1607
- user: userSchema$1.optional(),
1608
- parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
1609
- inline: inlineSchema.optional(),
1610
- deleted: z.boolean().optional(),
1611
- resolution: z.looseObject({}).optional()
1612
- });
1613
- function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
1614
- const workspace = env.BITBUCKET_WORKSPACE;
1615
- const repository = env.BITBUCKET_REPO_SLUG;
1616
- const token = env.BITBUCKET_API_TOKEN;
1617
- const email = env.BITBUCKET_EMAIL;
1618
- if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
1619
- if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
1620
- if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
1621
- const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
1622
- const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
1623
- const api = createCodeHostHttpClient({
1624
- baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
1625
- headers: { Authorization: authorization },
1626
- fetch
1627
- });
1628
- const rootApi = createCodeHostHttpClient({
1629
- baseUrl: "https://api.bitbucket.org/2.0/",
1630
- headers: { Authorization: authorization },
1631
- fetch
1632
- });
1633
- const prPath = (id) => `pullrequests/${id}`;
1634
- return {
1635
- workspace,
1636
- repository,
1637
- async currentUser() {
1638
- const value = await rootApi.json("user", userSchema$1);
1639
- return {
1640
- uuid: value.uuid,
1641
- nickname: value.nickname,
1642
- displayName: value.display_name
1643
- };
1644
- },
1645
- async getRepository() {
1646
- const value = await api.json("", bitbucketRepositorySchema);
1647
- return {
1648
- uuid: value.uuid,
1649
- slug: value.slug,
1650
- fullName: value.full_name,
1651
- url: value.links.html.href
1652
- };
1653
- },
1654
- async getRepositoryPermission(actor, repositoryUuid) {
1655
- const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
1656
- const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
1657
- if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
1658
- const permissionApi = createCodeHostHttpClient({
1659
- baseUrl: "https://api.bitbucket.org/2.0/",
1660
- headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
1661
- fetch
1662
- });
1663
- const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
1664
- return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
1665
- permission: z.enum([
1666
- "read",
1667
- "write",
1668
- "admin"
1669
- ]),
1670
- user: userSchema$1
1671
- })))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
1672
- },
1673
- getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
1674
- async loadChange(options) {
1675
- if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
1676
- const pullRequest = await this.getPullRequest(options.changeNumber);
1677
- return {
1678
- repository: {
1679
- slug: pullRequest.destination.repository.full_name,
1680
- url: pullRequest.destination.repository.links.html.href
1681
- },
1682
- coordinates: {
1683
- provider: "bitbucket",
1684
- workspace,
1685
- repository,
1686
- repositoryUuid: pullRequest.destination.repository.uuid
1687
- },
1688
- change: {
1689
- number: pullRequest.id,
1690
- isDraft: pullRequest.draft,
1691
- title: pullRequest.title,
1692
- description: pullRequest.description,
1693
- url: pullRequest.links.html.href,
1694
- author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
1695
- base: {
1696
- sha: pullRequest.destination.commit.hash,
1697
- ref: pullRequest.destination.branch.name,
1698
- url: pullRequest.destination.repository.links.html.href
1699
- },
1700
- head: {
1701
- sha: pullRequest.source.commit.hash,
1702
- ref: pullRequest.source.branch.name,
1703
- url: pullRequest.source.repository.links.html.href
1704
- },
1705
- isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
1706
- }
1707
- };
1708
- },
1709
- async listComments(id) {
1710
- return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
1711
- },
1712
- createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
1713
- updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
1714
- async replyToComment(id, commentId, content) {
1715
- const parentId = positiveCommentId(commentId);
1716
- return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
1717
- content: { raw: content },
1718
- parent: { id: parentId }
1719
- }));
1720
- },
1721
- async resolveComment(id, commentId) {
1722
- positiveCommentId(commentId);
1723
- await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
1724
- },
1725
- async setStatus(sha, key, body) {
1726
- return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
1727
- ...body,
1728
- key
1729
- }))).key;
1730
- }
1731
- };
1732
- }
1733
- function positiveCommentId(value) {
1734
- const id = Number(value);
1735
- if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
1736
- return id;
1737
- }
1738
- function bitbucketStatusState(state) {
1739
- if (state === "pending") return "INPROGRESS";
1740
- if (state === "failure") return "FAILED";
1741
- if (state === "neutral") return "STOPPED";
1742
- return "SUCCESSFUL";
1743
- }
1744
- function escapeBitbucketQueryValue(value) {
1745
- return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
1746
- }
1747
- function pagedSchema(item) {
1748
- return z.looseObject({
1749
- values: z.array(item),
1750
- next: z.string().url().optional()
1751
- });
1752
- }
1753
- async function listAll(api, path, schema, allowedPathPrefix) {
1754
- const values = [];
1755
- let next = path;
1756
- while (next) {
1757
- const page = await api.json(next, pagedSchema(schema));
1758
- values.push(...page.values);
1759
- if (page.next) {
1760
- const url = new URL(page.next);
1761
- if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
1762
- }
1763
- next = page.next;
1764
- }
1765
- return values;
1766
- }
1767
- function jsonRequest$1(method, body) {
1768
- return {
1769
- method,
1770
- headers: { "Content-Type": "application/json" },
1771
- body: JSON.stringify(body)
1772
- };
1773
- }
1774
- //#endregion
1775
- //#region src/hosts/gitlab/client.ts
1776
- const userSchema = z.looseObject({
1777
- id: z.number().int().positive(),
1778
- username: z.string().min(1)
1779
- });
1780
- const noteSchema = z.looseObject({
1781
- id: z.union([z.number(), z.string()]).transform(String),
1782
- body: z.string(),
1783
- author: userSchema.optional()
1784
- });
1785
- const positionSchema = z.looseObject({
1786
- new_path: z.string().optional(),
1787
- old_path: z.string().optional(),
1788
- new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
1789
- old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
1790
- line_range: z.looseObject({
1791
- start: z.looseObject({
1792
- new_line: z.number().int().positive().nullish(),
1793
- old_line: z.number().int().positive().nullish()
1794
- }),
1795
- end: z.looseObject({
1796
- new_line: z.number().int().positive().nullish(),
1797
- old_line: z.number().int().positive().nullish()
1798
- })
1799
- }).optional()
1800
- });
1801
- const discussionNoteSchema = noteSchema.extend({
1802
- resolvable: z.boolean().optional(),
1803
- resolved: z.boolean().optional(),
1804
- position: positionSchema.nullable().optional()
1805
- });
1806
- const discussionSchema = z.looseObject({
1807
- id: z.string().min(1),
1808
- individual_note: z.boolean().optional(),
1809
- notes: z.array(discussionNoteSchema)
1810
- });
1811
- const diffRefsSchema = z.looseObject({
1812
- base_sha: z.string().min(1),
1813
- start_sha: z.string().min(1),
1814
- head_sha: z.string().min(1)
1815
- });
1816
- const mergeRequestResponseSchema = z.looseObject({
1817
- iid: z.number().int().positive(),
1818
- title: z.string().default(""),
1819
- description: z.string().nullable().optional(),
1820
- web_url: z.string().url().optional(),
1821
- source_branch: z.string().min(1),
1822
- target_branch: z.string().min(1),
1823
- source_project_id: z.number().int().positive(),
1824
- target_project_id: z.number().int().positive(),
1825
- sha: z.string().min(1),
1826
- draft: z.boolean().optional(),
1827
- author: userSchema.optional(),
1828
- references: z.looseObject({ full: z.string().optional() }).optional(),
1829
- diff_refs: diffRefsSchema.nullable().optional()
1830
- });
1831
- const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
1832
- const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
1833
- const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
1834
- function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
1835
- const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
1836
- if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
1837
- const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
1838
- const clientOptions = {
1839
- baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
1840
- headers,
1841
- fetch,
1842
- sleep: async (milliseconds) => void await sleep(milliseconds)
1843
- };
1844
- const api = createCodeHostHttpClient(clientOptions);
1845
- const statusApi = createCodeHostHttpClient({
1846
- ...clientOptions,
1847
- retryNonIdempotentStatuses: [409]
1848
- });
1849
- return {
1850
- async getProject(project) {
1851
- const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
1852
- id: z.union([z.number(), z.string()]).transform(String),
1853
- path_with_namespace: z.string().min(1)
1854
- }));
1855
- return {
1856
- id: value.id,
1857
- path: value.path_with_namespace
1858
- };
1859
- },
1860
- currentUser: () => api.json("user", userSchema),
1861
- async loadChange(options) {
1862
- const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
1863
- return {
1864
- repository: {
1865
- slug: options.projectPath,
1866
- url: projectWebUrl(mergeRequest.web_url)
1867
- },
1868
- coordinates: {
1869
- provider: "gitlab",
1870
- projectId: String(mergeRequest.target_project_id),
1871
- projectPath: options.projectPath
1872
- },
1873
- change: {
1874
- number: mergeRequest.iid,
1875
- title: mergeRequest.title,
1876
- description: mergeRequest.description ?? "",
1877
- url: mergeRequest.web_url,
1878
- author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
1879
- base: {
1880
- sha: mergeRequest.diff_refs.start_sha,
1881
- ref: mergeRequest.target_branch
1882
- },
1883
- head: {
1884
- sha: mergeRequest.diff_refs.head_sha,
1885
- ref: mergeRequest.source_branch
1886
- },
1887
- isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
1888
- isDraft: mergeRequest.draft
1889
- }
1890
- };
1891
- },
1892
- async getMergeRequest(projectId, changeNumber) {
1893
- const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
1894
- for (let attempt = 0;; attempt += 1) {
1895
- const response = await api.json(requestPath, mergeRequestResponseSchema);
1896
- const prepared = mergeRequestSchema.safeParse(response);
1897
- if (prepared.success) return prepared.data;
1898
- if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
1899
- await sleep(250 * 2 ** attempt);
1900
- }
1901
- },
1902
- async getRepositoryPermission(projectId, username) {
1903
- const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
1904
- if (!user) return "none";
1905
- try {
1906
- return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
1907
- } catch (error) {
1908
- if (error instanceof CodeHostHttpError && error.status === 404) return "none";
1909
- throw error;
1910
- }
1911
- },
1912
- listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
1913
- createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
1914
- updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
1915
- listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
1916
- getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
1917
- async findReplyParent(projectId, changeNumber, noteId, discussionId) {
1918
- if (!discussionId) return void 0;
1919
- const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
1920
- if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
1921
- return discussion.notes[0]?.id;
1922
- },
1923
- createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
1924
- body,
1925
- position
1926
- })),
1927
- replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
1928
- async resolveDiscussion(projectId, changeNumber, discussionId) {
1929
- await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
1930
- },
1931
- async setStatus(projectId, sha, name, state, description) {
1932
- const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
1933
- state: gitLabStatusState(state),
1934
- name,
1935
- description: boundedStatusDescription(description)
1936
- }));
1937
- return String(status.id ?? name);
1938
- }
1939
- };
1940
- }
1941
- const MAX_PAGINATION_PAGES = 100;
1942
- async function paginated(client, path, schema) {
1943
- const values = [];
1944
- for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
1945
- const separator = path.includes("?") ? "&" : "?";
1946
- const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
1947
- values.push(...batch);
1948
- if (batch.length < 100) return values;
1949
- }
1950
- throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
1951
- }
1952
- function jsonRequest(method, body) {
1953
- return {
1954
- method,
1955
- headers: { "Content-Type": "application/json" },
1956
- body: JSON.stringify(body)
1957
- };
1958
- }
1959
- function accessLevelPermission(level) {
1960
- if (level >= 50) return "admin";
1961
- if (level >= 40) return "maintain";
1962
- if (level >= 30) return "write";
1963
- if (level >= 15) return "triage";
1964
- if (level >= 10) return "read";
1965
- return "none";
1966
- }
1967
- function boundedStatusDescription(description) {
1968
- return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
1969
- }
1970
- function gitLabStatusState(state) {
1971
- switch (state) {
1972
- case "pending": return "pending";
1973
- case "success": return "success";
1974
- case "failure": return "failed";
1975
- case "neutral": return "skipped";
1976
- }
1977
- }
1978
- function projectWebUrl(mergeRequestUrl) {
1979
- return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
1980
- }
1981
- //#endregion
1982
- //#region src/diff/git.ts
1983
- function runGit$1(args, cwd, maxBuffer) {
1984
- const result = Bun.spawnSync(["git", ...args], {
1985
- cwd,
1986
- env: process.env,
1987
- maxBuffer,
1988
- stderr: "pipe",
1989
- stdout: "pipe"
1990
- });
1991
- if (result.exitCode !== 0) {
1992
- const failure = result.stderr?.toString().trim() || "unknown error";
1993
- throw new Error(`git ${args.join(" ")} failed: ${failure}`);
1994
- }
1995
- return result.stdout.toString();
1996
- }
1997
- //#endregion
1998
- //#region src/diff/diff.ts
1999
- const lockFilePattern = /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock)$/;
2000
- const generatedPattern = /(^|\/)(dist|build|coverage|vendor)\//;
2001
- const maxInlineChangedLines = 1e3;
2002
- const maxCommentableRangeLines = 5e3;
2003
- function buildDiffManifest(options) {
2004
- const mergeBaseSha = runGit$1([
2005
- "merge-base",
2006
- options.baseSha,
2007
- options.headSha
2008
- ], options.cwd).trim();
2009
- const diffHead = options.includeWorkingTree ? void 0 : options.headSha;
2010
- const nameStatus = runGit$1(buildDiffArgs([
2011
- "--name-status",
2012
- "-z",
2013
- "--find-renames"
2014
- ], mergeBaseSha, diffHead), options.cwd);
2015
- const diffStats = getDiffStats(options.cwd, mergeBaseSha, diffHead);
2016
- const rawPatch = parseRawPatch(runGit$1(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(diffStats)), options.cwd));
2017
- const files = parseNameStatus(nameStatus);
2018
- const parsedDiff = parseUnifiedDiff(rawPatch.patch, rawPatch.filePaths);
2019
- for (const file of files) {
2020
- const stats = diffStats.get(file.path);
2021
- file.additions = stats?.additions ?? 0;
2022
- file.deletions = stats?.deletions ?? 0;
2023
- const preExcludedReason = stats?.excludedReason;
2024
- const parsedFile = parsedDiff.get(file.path);
2025
- file.hunks = preExcludedReason ? [] : parsedFile?.hunks ?? [];
2026
- file.commentableRanges = preExcludedReason ? [] : parsedFile?.commentableRanges ?? [];
2027
- const excludedReason = preExcludedReason ?? getExcludedReason(file);
2028
- if (excludedReason) {
2029
- file.hunks = [];
2030
- file.commentableRanges = [];
2031
- file.excludedReason = excludedReason;
2032
- }
2033
- }
2034
- return parseDiffManifest({
2035
- baseSha: options.baseSha,
2036
- headSha: options.headSha,
2037
- mergeBaseSha,
2038
- files
2039
- });
2040
- }
2041
- function parseNameStatus(output) {
2042
- const fields = output.split("\0");
2043
- const files = [];
2044
- let index = 0;
2045
- while (index < fields.length) {
2046
- const rawStatus = fields[index++];
2047
- if (!rawStatus) continue;
2048
- const firstPath = fields[index++] ?? "";
2049
- const status = parseFileStatus(rawStatus);
2050
- if (status === "renamed") {
2051
- const secondPath = fields[index++] ?? "";
2052
- files.push(baseFile(secondPath || firstPath, status, firstPath));
2053
- continue;
2054
- }
2055
- files.push(baseFile(firstPath, status));
2056
- }
2057
- return files;
2058
- }
2059
- function parseUnifiedDiff(diff, filePaths) {
2060
- const state = createDiffParserState(filePaths);
2061
- for (const line of diff.split("\n")) parseUnifiedDiffLine(state, line);
2062
- finishActiveHunk(state);
2063
- return state.filesByPath;
2064
- }
2065
- function getDiffStats(cwd, baseSha, headSha) {
2066
- const output = runGit$1(buildDiffArgs([
2067
- "--numstat",
2068
- "-z",
2069
- "--find-renames"
2070
- ], baseSha, headSha), cwd);
2071
- const stats = /* @__PURE__ */ new Map();
2072
- for (const stat of parseNumstat(output)) {
2073
- if (stat.binary) {
2074
- stats.set(stat.path, {
2075
- additions: 0,
2076
- deletions: 0,
2077
- excludedReason: "binary diff"
2078
- });
2079
- continue;
2080
- }
2081
- stats.set(stat.path, {
2082
- additions: stat.additions,
2083
- deletions: stat.deletions,
2084
- excludedReason: stat.additions + stat.deletions > maxInlineChangedLines ? "oversized diff" : void 0
2085
- });
2086
- }
2087
- return stats;
2088
- }
2089
- function getPreExcludedFiles(stats) {
2090
- const excluded = /* @__PURE__ */ new Map();
2091
- for (const [filePath, stat] of stats) if (stat.excludedReason) excluded.set(filePath, stat.excludedReason);
2092
- return excluded;
2093
- }
2094
- function buildUnifiedDiffArgs(baseSha, headSha, excludedFiles) {
2095
- const args = buildDiffArgs([
2096
- "--raw",
2097
- "-z",
2098
- "--patch",
2099
- "--submodule=short",
2100
- "--unified=80",
2101
- "--find-renames"
2102
- ], baseSha, headSha);
2103
- if (excludedFiles.size === 0) return args;
2104
- return [
2105
- ...args,
2106
- "--",
2107
- ".",
2108
- ...[...excludedFiles.keys()].map((filePath) => `:(exclude,literal)${filePath}`)
2109
- ];
2110
- }
2111
- function buildDiffArgs(options, baseSha, headSha) {
2112
- return headSha ? [
2113
- "diff",
2114
- ...options,
2115
- baseSha,
2116
- headSha
2117
- ] : [
2118
- "diff",
2119
- ...options,
2120
- baseSha
2121
- ];
2122
- }
2123
- function parseNumstat(output) {
2124
- const fields = output.split("\0");
2125
- const stats = [];
2126
- let index = 0;
2127
- while (index < fields.length) {
2128
- const header = parseNumstatHeader(fields[index++] ?? "");
2129
- if (!header) continue;
2130
- const resolvedPath = resolveNumstatPath(header.path, fields, index);
2131
- index = resolvedPath.nextIndex;
2132
- const filePath = resolvedPath.path;
2133
- if (!filePath) continue;
2134
- const binary = header.rawAdditions === "-" || header.rawDeletions === "-";
2135
- stats.push({
2136
- path: filePath,
2137
- additions: binary ? 0 : Number(header.rawAdditions),
2138
- deletions: binary ? 0 : Number(header.rawDeletions),
2139
- binary
2140
- });
2141
- }
2142
- return stats;
1303
+ function parseNumstat(output) {
1304
+ const fields = output.split("\0");
1305
+ const stats = [];
1306
+ let index = 0;
1307
+ while (index < fields.length) {
1308
+ const header = parseNumstatHeader(fields[index++] ?? "");
1309
+ if (!header) continue;
1310
+ const resolvedPath = resolveNumstatPath(header.path, fields, index);
1311
+ index = resolvedPath.nextIndex;
1312
+ const filePath = resolvedPath.path;
1313
+ if (!filePath) continue;
1314
+ const binary = header.rawAdditions === "-" || header.rawDeletions === "-";
1315
+ stats.push({
1316
+ path: filePath,
1317
+ additions: binary ? 0 : Number(header.rawAdditions),
1318
+ deletions: binary ? 0 : Number(header.rawDeletions),
1319
+ binary
1320
+ });
1321
+ }
1322
+ return stats;
2143
1323
  }
2144
1324
  function resolveNumstatPath(path, fields, nextIndex) {
2145
1325
  if (path) return {
@@ -2202,11 +1382,15 @@ function parseFileStatus(rawStatus) {
2202
1382
  return "modified";
2203
1383
  }
2204
1384
  function getExcludedReason(file) {
1385
+ const pathExcludedReason = getPathExcludedReason(file);
1386
+ if (pathExcludedReason) return pathExcludedReason;
1387
+ if (file.additions + file.deletions > maxInlineChangedLines) return "oversized diff";
1388
+ if (commentableRangeLineCount(file.commentableRanges) > maxCommentableRangeLines) return "oversized diff";
1389
+ }
1390
+ function getPathExcludedReason(file) {
2205
1391
  if (file.status === "removed") return "removed file";
2206
1392
  if (lockFilePattern.test(file.path)) return "lock file";
2207
1393
  if (generatedPattern.test(file.path)) return "generated or build output";
2208
- if (file.additions + file.deletions > maxInlineChangedLines) return "oversized diff";
2209
- if (commentableRangeLineCount(file.commentableRanges) > maxCommentableRangeLines) return "oversized diff";
2210
1394
  }
2211
1395
  function commentableRangeLineCount(ranges) {
2212
1396
  return ranges.reduce((total, range) => total + range.endLine - range.startLine + 1, 0);
@@ -3693,7 +2877,7 @@ function createRuntimeLog(options) {
3693
2877
  },
3694
2878
  text(level, event, text) {
3695
2879
  if (level === "debug" && !debugEnabled) return;
3696
- emitRecord(sink, secrets, level, event, void 0, redact(text, secrets));
2880
+ emitRecord(sink, secrets, level, event, void 0, redact$1(text, secrets));
3697
2881
  },
3698
2882
  textSnippet(level, event, text, snippetOptions) {
3699
2883
  if (level === "debug" && !debugEnabled) return;
@@ -3703,7 +2887,7 @@ function createRuntimeLog(options) {
3703
2887
  return formatTextSnippet(text, secrets, snippetOptions);
3704
2888
  },
3705
2889
  async group(name, run) {
3706
- return await sink.group(redact(name, secrets), run);
2890
+ return await sink.group(redact$1(name, secrets), run);
3707
2891
  },
3708
2892
  addSecret(value) {
3709
2893
  addSecret(secrets, value);
@@ -3726,25 +2910,25 @@ function boundedLogSnippet(text, options) {
3726
2910
  function addSecret(secrets, value) {
3727
2911
  if (value && value.length >= 4) secrets.add(value);
3728
2912
  }
3729
- function redact(message, secrets) {
2913
+ function redact$1(message, secrets) {
3730
2914
  let redacted = message;
3731
2915
  for (const secret of secrets) redacted = redacted.split(secret).join("***");
3732
2916
  return redacted;
3733
2917
  }
3734
2918
  function compactFields(fields, secrets) {
3735
2919
  const compact = {};
3736
- for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string") compact[key] = redact(value, secrets);
3737
- else if (Array.isArray(value)) compact[key] = value.map((item) => redact(item, secrets));
2920
+ for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string") compact[key] = redact$1(value, secrets);
2921
+ else if (Array.isArray(value)) compact[key] = value.map((item) => redact$1(item, secrets));
3738
2922
  else if (value !== void 0) compact[key] = value;
3739
2923
  return compact;
3740
2924
  }
3741
2925
  function formatTextSnippet(text, secrets, options) {
3742
- return boundedLogSnippet(redact(text, secrets), options);
2926
+ return boundedLogSnippet(redact$1(text, secrets), options);
3743
2927
  }
3744
2928
  function emitRecord(sink, secrets, level, event, fields, text) {
3745
2929
  sink.log({
3746
2930
  level,
3747
- event: redact(event, secrets),
2931
+ event: redact$1(event, secrets),
3748
2932
  fields: compactFields(fields, secrets),
3749
2933
  text
3750
2934
  });
@@ -5981,300 +5165,711 @@ async function runTaskRuntime(options) {
5981
5165
  const verifier = await runSynchronizeVerifier({
5982
5166
  options,
5983
5167
  config,
5984
- provider,
5985
- diffManifest,
5986
- priorReviewState,
5987
- runId,
5988
- piRunSink: runtimeOptions.piRunSink
5168
+ provider,
5169
+ diffManifest,
5170
+ priorReviewState,
5171
+ runId,
5172
+ piRunSink: runtimeOptions.piRunSink
5173
+ });
5174
+ const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
5175
+ const redactedPublication = redactReviewPublication({
5176
+ main,
5177
+ validated,
5178
+ threadActions: verifier.threadActions,
5179
+ taskChecks,
5180
+ redactor: options.secretRedactor
5181
+ });
5182
+ const publishing = buildCommentPublishingPlan({
5183
+ event: options.event,
5184
+ main: redactedPublication.main,
5185
+ validated: redactedPublication.validated,
5186
+ manifest: diffManifest,
5187
+ maxInlineComments: config.publication.maxInlineComments,
5188
+ maxStoredFindings: config.publication.maxStoredFindings,
5189
+ showHeader: config.publication.showHeader,
5190
+ showFooter: config.publication.showFooter,
5191
+ showStats: config.publication.showStats,
5192
+ priorReviewState: verifier.priorReviewState,
5193
+ threadActions: redactedPublication.threadActions,
5194
+ metadata: {
5195
+ runtimeVersion,
5196
+ configVersion: options.versionCompatibility?.configVersion,
5197
+ trustedConfigSha: options.trustedConfigSha,
5198
+ trustedConfigHash: options.trustedConfigHash,
5199
+ reviewedHeadSha: options.event.change.head.sha,
5200
+ providerModels: output.providerModels.length + verifier.providerModels.length > 0 ? uniq([...output.providerModels, ...verifier.providerModels]) : [provider.model],
5201
+ selectedTasks,
5202
+ failedTasks: [],
5203
+ validFindings: validated.validFindings.length,
5204
+ droppedFindings: validated.droppedFindings.length,
5205
+ ...stats ? { stats } : {}
5206
+ }
5207
+ });
5208
+ const publicationPlan = publishing.publicationPlan;
5209
+ publishTaskChecks(options.checkSink, redactedPublication.taskChecks);
5210
+ options.log?.info("review validated", {
5211
+ validFindings: validated.validFindings.length,
5212
+ droppedFindings: validated.droppedFindings.length,
5213
+ inlineDrafts: publishing.inlineCommentDrafts.length,
5214
+ threadActions: verifier.threadActions.length
5215
+ });
5216
+ return {
5217
+ kind: "review",
5218
+ provider,
5219
+ diffManifest,
5220
+ review: redactedPublication.validated.review,
5221
+ validated: redactedPublication.validated,
5222
+ publicationPlan,
5223
+ mainComment: publicationPlan.mainComment,
5224
+ inlineCommentDrafts: publishing.inlineCommentDrafts,
5225
+ taskChecks: redactedPublication.taskChecks,
5226
+ repairAttempted: output.repairAttempted
5227
+ };
5228
+ }
5229
+ function publishFailedRunTaskChecks(options, taskChecks) {
5230
+ const redacted = redactCommandPublication({
5231
+ body: "",
5232
+ taskChecks,
5233
+ redactor: options.secretRedactor
5234
+ });
5235
+ publishTaskChecks(options.checkSink, redacted.taskChecks);
5236
+ }
5237
+ function commandResponseResultFromOutput(options) {
5238
+ const commandResponse = options.output.commandResponse;
5239
+ if (!commandResponse) return;
5240
+ if (!options.commandInvocation) throw new Error("ctx.command.reply(...) is only available for command-triggered tasks");
5241
+ return commandResponseRuntimeResult({
5242
+ ...options,
5243
+ commandResponse,
5244
+ commandInvocation: options.commandInvocation
5245
+ });
5246
+ }
5247
+ function assertReviewCommentOutput(output, hasCommandInvocation) {
5248
+ if (output.comment) return;
5249
+ throw new Error(hasCommandInvocation ? "ctx.comment(...) or ctx.command.reply(...) must be called exactly once per selected run" : "ctx.comment(...) must be called exactly once per selected run");
5250
+ }
5251
+ async function runSynchronizeVerifier(options) {
5252
+ if (options.options.event.rawAction !== "synchronize") return {
5253
+ priorReviewState: options.priorReviewState,
5254
+ threadActions: [],
5255
+ providerModels: []
5256
+ };
5257
+ const config = options.config;
5258
+ return await runInternalVerifier({
5259
+ workspace: options.options.workspace,
5260
+ config,
5261
+ event: options.options.event,
5262
+ provider: options.provider,
5263
+ verifierProvider: resolveProvider(config, config.publication.autoResolve.model ?? config.defaultProvider),
5264
+ plan: options.options.plan,
5265
+ env: options.options.env,
5266
+ piExecutable: options.options.piExecutable,
5267
+ piRunner: options.options.piRunner,
5268
+ log: options.options.log,
5269
+ diffManifest: options.diffManifest,
5270
+ priorReviewState: options.priorReviewState,
5271
+ threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
5272
+ mode: { kind: "synchronize" },
5273
+ runId: options.runId,
5274
+ piRunSink: options.piRunSink
5989
5275
  });
5990
- const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
5991
- const redactedPublication = redactReviewPublication({
5992
- main,
5993
- validated,
5994
- threadActions: verifier.threadActions,
5995
- taskChecks,
5276
+ }
5277
+ function createTaskContext(options) {
5278
+ const repositorySlugParts = options.event.repository.slug.split("/");
5279
+ let taskContext;
5280
+ taskContext = {
5281
+ run: { id: options.runId },
5282
+ repository: {
5283
+ root: options.workspace,
5284
+ owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
5285
+ name: repositorySlugParts.at(-1) ?? "repo"
5286
+ },
5287
+ change: {
5288
+ number: options.event.change.number,
5289
+ title: options.event.change.title,
5290
+ description: options.event.change.description,
5291
+ url: options.event.change.url,
5292
+ author: options.event.change.author,
5293
+ base: options.event.change.base,
5294
+ head: options.event.change.head,
5295
+ isFork: options.event.change.isFork,
5296
+ async diffManifest(manifestOptions) {
5297
+ const key = JSON.stringify(manifestOptions ?? {});
5298
+ const cached = options.manifestCache.get(key);
5299
+ if (cached) return cloneDiffManifest(cached);
5300
+ const manifest = projectDiffManifest(options.diffManifest, manifestOptions);
5301
+ options.manifestCache.set(key, manifest);
5302
+ return cloneDiffManifest(manifest);
5303
+ },
5304
+ async changedFiles() {
5305
+ return options.diffManifest.files.map((file) => ({
5306
+ path: file.path,
5307
+ previousPath: file.previousPath,
5308
+ status: file.status
5309
+ }));
5310
+ },
5311
+ async currentHeadSha() {
5312
+ return options.event.change.head.sha;
5313
+ }
5314
+ },
5315
+ platform: { id: options.event.platform.id },
5316
+ command: options.commandInvocation ? {
5317
+ name: options.commandInvocation.name,
5318
+ line: options.commandInvocation.line,
5319
+ arguments: { ...options.commandInvocation.arguments },
5320
+ async reply(markdown) {
5321
+ collectCommandResponse(options.output, markdown, options.taskName);
5322
+ }
5323
+ } : void 0,
5324
+ secret(secret) {
5325
+ return resolveTaskSecret(secret, options);
5326
+ },
5327
+ pi: { async run(agent, input, runOptions) {
5328
+ const result = await runReviewAgent({
5329
+ agent,
5330
+ input,
5331
+ runOptions,
5332
+ runtime: {
5333
+ ...options,
5334
+ taskContext,
5335
+ runId: options.runId,
5336
+ piRunSink: options.piRunSink
5337
+ }
5338
+ });
5339
+ options.output.providerModels.push(...result.providerModels);
5340
+ if (result.repairAttempted) options.output.repairAttempted = true;
5341
+ trackResultFindingScope(options.output, result.value, runOptions?.paths);
5342
+ return agentOutputForTaskContext(agent, result.value);
5343
+ } },
5344
+ review: { async prior() {
5345
+ return priorReviewForTask(options.priorMainComment, options.priorReviewState);
5346
+ } },
5347
+ check: createCheckHandle(options.output),
5348
+ async comment(value) {
5349
+ collectComment(options.output, value, options.taskName);
5350
+ },
5351
+ log: options.taskLog ?? console
5352
+ };
5353
+ return taskContext;
5354
+ }
5355
+ function agentOutputForTaskContext(_agent, value) {
5356
+ return value;
5357
+ }
5358
+ function resolveTaskSecret(secret, options) {
5359
+ if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
5360
+ const value = (options.env ?? process.env)[secret.name];
5361
+ if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
5362
+ options.log?.addSecret(value);
5363
+ options.secretRedactor?.addSecret(value);
5364
+ return value;
5365
+ }
5366
+ function commandResponseRuntimeResult(options) {
5367
+ const redacted = redactCommandPublication({
5368
+ body: options.commandResponse.value,
5369
+ taskChecks: options.taskChecks,
5996
5370
  redactor: options.secretRedactor
5997
5371
  });
5998
- const publishing = buildCommentPublishingPlan({
5372
+ return {
5373
+ kind: "command-response",
5374
+ provider: options.provider,
5375
+ diffManifest: options.diffManifest,
5376
+ taskChecks: redacted.taskChecks,
5377
+ repairAttempted: options.output.repairAttempted,
5378
+ commandResponse: {
5379
+ commandName: options.commandInvocation.name,
5380
+ line: options.commandInvocation.line,
5381
+ arguments: options.commandInvocation.arguments,
5382
+ body: redacted.body
5383
+ }
5384
+ };
5385
+ }
5386
+ function publishTaskChecks(sink, checks) {
5387
+ for (const check of checks) sink?.setTaskResult(check);
5388
+ }
5389
+ function skippedTaskRuntimeResult(options) {
5390
+ const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
5391
+ const review = {
5392
+ summary: { body: reason },
5393
+ inlineFindings: []
5394
+ };
5395
+ const validated = {
5396
+ review,
5397
+ validFindings: [],
5398
+ droppedFindings: []
5399
+ };
5400
+ const publicationPlan = buildCommentPublishingPlan({
5999
5401
  event: options.event,
6000
- main: redactedPublication.main,
6001
- validated: redactedPublication.validated,
6002
- manifest: diffManifest,
6003
- maxInlineComments: config.publication.maxInlineComments,
6004
- maxStoredFindings: config.publication.maxStoredFindings,
6005
- showHeader: config.publication.showHeader,
6006
- showFooter: config.publication.showFooter,
6007
- showStats: config.publication.showStats,
6008
- priorReviewState: verifier.priorReviewState,
6009
- threadActions: redactedPublication.threadActions,
5402
+ main: reason,
5403
+ validated,
5404
+ manifest: options.diffManifest,
5405
+ maxInlineComments: options.config.publication.maxInlineComments,
5406
+ maxStoredFindings: options.config.publication.maxStoredFindings,
5407
+ showHeader: options.config.publication.showHeader,
5408
+ showFooter: options.config.publication.showFooter,
5409
+ showStats: options.config.publication.showStats,
6010
5410
  metadata: {
6011
5411
  runtimeVersion,
6012
5412
  configVersion: options.versionCompatibility?.configVersion,
6013
5413
  trustedConfigSha: options.trustedConfigSha,
6014
5414
  trustedConfigHash: options.trustedConfigHash,
6015
5415
  reviewedHeadSha: options.event.change.head.sha,
6016
- providerModels: output.providerModels.length + verifier.providerModels.length > 0 ? uniq([...output.providerModels, ...verifier.providerModels]) : [provider.model],
6017
- selectedTasks,
5416
+ providerModels: [options.provider.model],
5417
+ selectedTasks: [],
6018
5418
  failedTasks: [],
6019
- validFindings: validated.validFindings.length,
6020
- droppedFindings: validated.droppedFindings.length,
6021
- ...stats ? { stats } : {}
5419
+ validFindings: 0,
5420
+ droppedFindings: 0
5421
+ }
5422
+ }).publicationPlan;
5423
+ return {
5424
+ kind: "skipped",
5425
+ skipReason: reason,
5426
+ provider: options.provider,
5427
+ diffManifest: options.diffManifest,
5428
+ review,
5429
+ validated,
5430
+ publicationPlan,
5431
+ mainComment: publicationPlan.mainComment,
5432
+ inlineCommentDrafts: [],
5433
+ taskChecks: [],
5434
+ repairAttempted: false
5435
+ };
5436
+ }
5437
+ //#endregion
5438
+ //#region src/shared/secret-redactor-core.ts
5439
+ const redactedSecret = "[redacted secret]";
5440
+ const minimumSecretLength = 4;
5441
+ function createSecretRedactor(initialSecrets) {
5442
+ const secrets = new Set(initialSecrets.filter((value) => value.length >= minimumSecretLength));
5443
+ return {
5444
+ addSecret(value) {
5445
+ if (value && value.length >= minimumSecretLength) secrets.add(value);
5446
+ },
5447
+ redact(value) {
5448
+ const ordered = [...secrets].sort((left, right) => right.length - left.length);
5449
+ let next = value;
5450
+ for (const secret of ordered) next = next.replaceAll(secret, redactedSecret);
5451
+ return {
5452
+ value: next,
5453
+ detected: next !== value
5454
+ };
5455
+ }
5456
+ };
5457
+ }
5458
+ //#endregion
5459
+ //#region src/shared/secret-redactor.ts
5460
+ function createKnownSecretRedactor(options) {
5461
+ return createSecretRedactor(sensitiveEnvironmentValues(options?.env ?? process.env));
5462
+ }
5463
+ //#endregion
5464
+ //#region src/hosts/http.ts
5465
+ var CodeHostHttpError = class extends Error {
5466
+ status;
5467
+ constructor(message, status) {
5468
+ super(message);
5469
+ this.name = "CodeHostHttpError";
5470
+ this.status = status;
5471
+ }
5472
+ };
5473
+ function createCodeHostHttpClient(options) {
5474
+ const fetchRequest = options.fetch ?? fetch;
5475
+ const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
5476
+ const successThrottle = options.successThrottle ?? createCodeHostSuccessThrottle(sleep);
5477
+ const maxRetries = options.maxRetries ?? 2;
5478
+ const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
5479
+ const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
5480
+ return { async json(path, schema, init = {}) {
5481
+ const method = init.method?.toUpperCase() ?? "GET";
5482
+ for (let attempt = 0;; attempt += 1) {
5483
+ await successThrottle.wait();
5484
+ const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
5485
+ const response = await fetchRequest(new URL(path, options.baseUrl), {
5486
+ ...init,
5487
+ headers: {
5488
+ ...Object.fromEntries(new Headers(options.headers)),
5489
+ ...Object.fromEntries(new Headers(init.headers))
5490
+ },
5491
+ signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
5492
+ });
5493
+ const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
5494
+ if (response.ok) {
5495
+ successThrottle.update(response.headers.get("Retry-After"));
5496
+ return schema.parse(await response.json());
5497
+ }
5498
+ if (shouldRetryCodeHostRequest({
5499
+ method,
5500
+ attempt,
5501
+ maxRetries,
5502
+ response,
5503
+ retryStatuses: options.retryNonIdempotentStatuses
5504
+ })) {
5505
+ await sleep(retryAfter || 250 * 2 ** attempt);
5506
+ continue;
5507
+ }
5508
+ const body = (await response.text()).slice(0, 1024);
5509
+ throw new CodeHostHttpError(redact(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
6022
5510
  }
6023
- });
6024
- const publicationPlan = publishing.publicationPlan;
6025
- publishTaskChecks(options.checkSink, redactedPublication.taskChecks);
6026
- options.log?.info("review validated", {
6027
- validFindings: validated.validFindings.length,
6028
- droppedFindings: validated.droppedFindings.length,
6029
- inlineDrafts: publishing.inlineCommentDrafts.length,
6030
- threadActions: verifier.threadActions.length
6031
- });
5511
+ } };
5512
+ }
5513
+ function createCodeHostSuccessThrottle(sleep = (milliseconds) => Bun.sleep(milliseconds)) {
5514
+ let notBefore = 0;
5515
+ let waitInFlight;
5516
+ const waitUntilReady = async () => {
5517
+ while (true) {
5518
+ const deadline = notBefore;
5519
+ const remaining = deadline - Date.now();
5520
+ if (remaining <= 0) {
5521
+ if (notBefore === deadline) notBefore = 0;
5522
+ return;
5523
+ }
5524
+ await sleep(remaining);
5525
+ if (notBefore <= deadline) {
5526
+ notBefore = 0;
5527
+ return;
5528
+ }
5529
+ }
5530
+ };
6032
5531
  return {
6033
- kind: "review",
6034
- provider,
6035
- diffManifest,
6036
- review: redactedPublication.validated.review,
6037
- validated: redactedPublication.validated,
6038
- publicationPlan,
6039
- mainComment: publicationPlan.mainComment,
6040
- inlineCommentDrafts: publishing.inlineCommentDrafts,
6041
- taskChecks: redactedPublication.taskChecks,
6042
- repairAttempted: output.repairAttempted
5532
+ wait() {
5533
+ if (waitInFlight) return waitInFlight;
5534
+ const currentWait = waitUntilReady().finally(() => {
5535
+ if (waitInFlight === currentWait) waitInFlight = void 0;
5536
+ });
5537
+ waitInFlight = currentWait;
5538
+ return waitInFlight;
5539
+ },
5540
+ update(retryAfter) {
5541
+ const delay = retryAfterMilliseconds(retryAfter);
5542
+ if (delay > 0) notBefore = Math.max(notBefore, Date.now() + delay);
5543
+ }
6043
5544
  };
6044
5545
  }
6045
- function publishFailedRunTaskChecks(options, taskChecks) {
6046
- const redacted = redactCommandPublication({
6047
- body: "",
6048
- taskChecks,
6049
- redactor: options.secretRedactor
6050
- });
6051
- publishTaskChecks(options.checkSink, redacted.taskChecks);
5546
+ function shouldRetryCodeHostRequest(options) {
5547
+ return (options.method === "GET" || options.method === "HEAD" || options.retryStatuses?.includes(options.response.status) === true) && options.attempt < options.maxRetries && (options.response.status === 429 || options.response.status >= 500 || options.retryStatuses?.includes(options.response.status) === true);
6052
5548
  }
6053
- function commandResponseResultFromOutput(options) {
6054
- const commandResponse = options.output.commandResponse;
6055
- if (!commandResponse) return;
6056
- if (!options.commandInvocation) throw new Error("ctx.command.reply(...) is only available for command-triggered tasks");
6057
- return commandResponseRuntimeResult({
6058
- ...options,
6059
- commandResponse,
6060
- commandInvocation: options.commandInvocation
6061
- });
5549
+ function retryAfterMilliseconds(value) {
5550
+ if (!value) return 0;
5551
+ const seconds = Number(value);
5552
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
5553
+ const date = Date.parse(value);
5554
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
6062
5555
  }
6063
- function assertReviewCommentOutput(output, hasCommandInvocation) {
6064
- if (output.comment) return;
6065
- throw new Error(hasCommandInvocation ? "ctx.comment(...) or ctx.command.reply(...) must be called exactly once per selected run" : "ctx.comment(...) must be called exactly once per selected run");
5556
+ function redact(value, secrets) {
5557
+ let redacted = value;
5558
+ for (const secret of secrets) redacted = redacted.split(secret).join("***");
5559
+ return redacted;
6066
5560
  }
6067
- async function runSynchronizeVerifier(options) {
6068
- if (options.options.event.rawAction !== "synchronize") return {
6069
- priorReviewState: options.priorReviewState,
6070
- threadActions: [],
6071
- providerModels: []
5561
+ //#endregion
5562
+ //#region src/hosts/azure-devops/coordinates.ts
5563
+ function azureOrganizationFromUrl(value) {
5564
+ const url = new URL(value);
5565
+ return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
5566
+ }
5567
+ //#endregion
5568
+ //#region src/hosts/azure-devops/client.ts
5569
+ const identitySchema = z.looseObject({
5570
+ displayName: z.string().optional(),
5571
+ providerDisplayName: z.string().optional(),
5572
+ uniqueName: z.string().optional(),
5573
+ id: z.string().optional(),
5574
+ properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
5575
+ }).transform((identity) => {
5576
+ const displayName = identity.displayName ?? identity.providerDisplayName;
5577
+ const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
5578
+ return {
5579
+ ...identity,
5580
+ ...displayName ? { displayName } : {},
5581
+ ...uniqueName ? { uniqueName } : {}
6072
5582
  };
6073
- const config = options.config;
6074
- return await runInternalVerifier({
6075
- workspace: options.options.workspace,
6076
- config,
6077
- event: options.options.event,
6078
- provider: options.provider,
6079
- verifierProvider: resolveProvider(config, config.publication.autoResolve.model ?? config.defaultProvider),
6080
- plan: options.options.plan,
6081
- env: options.options.env,
6082
- piExecutable: options.options.piExecutable,
6083
- piRunner: options.options.piRunner,
6084
- log: options.options.log,
6085
- diffManifest: options.diffManifest,
6086
- priorReviewState: options.priorReviewState,
6087
- threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
6088
- mode: { kind: "synchronize" },
6089
- runId: options.runId,
6090
- piRunSink: options.piRunSink
5583
+ });
5584
+ const commitSchema = z.looseObject({ commitId: z.string().min(1) });
5585
+ const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
5586
+ const pullRequestSchema$1 = z.looseObject({
5587
+ pullRequestId: z.number().int().positive(),
5588
+ isDraft: z.boolean().optional(),
5589
+ title: z.string(),
5590
+ description: z.string().nullish(),
5591
+ url: z.string().optional(),
5592
+ sourceRefName: z.string().min(1),
5593
+ targetRefName: z.string().min(1),
5594
+ createdBy: identitySchema.optional(),
5595
+ lastMergeSourceCommit: commitSchema,
5596
+ lastMergeTargetCommit: commitSchema,
5597
+ forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
5598
+ repository: z.looseObject({
5599
+ id: z.string().min(1),
5600
+ name: z.string().min(1),
5601
+ url: z.string().optional(),
5602
+ project: z.looseObject({
5603
+ id: z.string().min(1),
5604
+ name: z.string().min(1)
5605
+ })
5606
+ })
5607
+ });
5608
+ const repositorySchema$1 = z.looseObject({
5609
+ id: z.string().min(1),
5610
+ name: z.string().min(1),
5611
+ project: z.looseObject({
5612
+ id: z.string().min(1),
5613
+ name: z.string().min(1)
5614
+ })
5615
+ });
5616
+ const iterationSchema = z.looseObject({
5617
+ id: z.number().int().positive(),
5618
+ sourceRefCommit: commitSchema
5619
+ });
5620
+ const iterationChangeSchema = z.looseObject({
5621
+ changeTrackingId: z.number().int(),
5622
+ changeType: z.string(),
5623
+ item: z.looseObject({
5624
+ path: z.string().min(1),
5625
+ originalPath: z.string().min(1).optional()
5626
+ })
5627
+ });
5628
+ const threadCommentSchema = z.looseObject({
5629
+ id: z.union([z.number(), z.string()]).transform(String),
5630
+ parentCommentId: z.number().int().optional(),
5631
+ content: z.string().default(""),
5632
+ author: identitySchema.optional(),
5633
+ isDeleted: z.boolean().optional()
5634
+ });
5635
+ const pointSchema = z.looseObject({
5636
+ line: z.number().int(),
5637
+ offset: z.number().int()
5638
+ });
5639
+ const threadSchema = z.looseObject({
5640
+ id: z.union([z.number(), z.string()]).transform(String),
5641
+ status: z.string().optional(),
5642
+ comments: z.array(threadCommentSchema).default([]),
5643
+ threadContext: z.looseObject({
5644
+ filePath: z.string().optional(),
5645
+ leftFileStart: pointSchema.nullish(),
5646
+ leftFileEnd: pointSchema.nullish(),
5647
+ rightFileStart: pointSchema.nullish(),
5648
+ rightFileEnd: pointSchema.nullish()
5649
+ }).nullish(),
5650
+ pullRequestThreadContext: z.looseObject({
5651
+ changeTrackingId: z.number().int().optional(),
5652
+ iterationContext: z.looseObject({
5653
+ firstComparingIteration: z.number().int(),
5654
+ secondComparingIteration: z.number().int()
5655
+ }).optional()
5656
+ }).nullish()
5657
+ });
5658
+ const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
5659
+ const aclSchema = collectionSchema(z.looseObject({
5660
+ inheritPermissions: z.boolean().default(true),
5661
+ acesDictionary: z.record(z.string(), z.looseObject({
5662
+ allow: z.number().int().default(0),
5663
+ deny: z.number().int().default(0),
5664
+ extendedInfo: z.looseObject({
5665
+ effectiveAllow: z.number().int().default(0),
5666
+ effectiveDeny: z.number().int().default(0)
5667
+ }).optional()
5668
+ }))
5669
+ }));
5670
+ function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch, options = {}) {
5671
+ const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
5672
+ const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
5673
+ const pat = env.AZURE_DEVOPS_TOKEN;
5674
+ const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
5675
+ if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
5676
+ if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
5677
+ if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
5678
+ const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
5679
+ const successThrottle = createCodeHostSuccessThrottle(options.sleep);
5680
+ const api = createCodeHostHttpClient({
5681
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
5682
+ headers,
5683
+ fetch,
5684
+ successThrottle
6091
5685
  });
6092
- }
6093
- function createTaskContext(options) {
6094
- const repositorySlugParts = options.event.repository.slug.split("/");
6095
- let taskContext;
6096
- taskContext = {
6097
- run: { id: options.runId },
6098
- repository: {
6099
- root: options.workspace,
6100
- owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
6101
- name: repositorySlugParts.at(-1) ?? "repo"
5686
+ const organizationApi = createCodeHostHttpClient({
5687
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
5688
+ headers,
5689
+ fetch,
5690
+ successThrottle
5691
+ });
5692
+ const identityApi = createCodeHostHttpClient({
5693
+ baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
5694
+ headers,
5695
+ fetch,
5696
+ successThrottle
5697
+ });
5698
+ const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
5699
+ return {
5700
+ organization,
5701
+ project,
5702
+ async currentUser() {
5703
+ return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
6102
5704
  },
6103
- change: {
6104
- number: options.event.change.number,
6105
- title: options.event.change.title,
6106
- description: options.event.change.description,
6107
- url: options.event.change.url,
6108
- author: options.event.change.author,
6109
- base: options.event.change.base,
6110
- head: options.event.change.head,
6111
- isFork: options.event.change.isFork,
6112
- async diffManifest(manifestOptions) {
6113
- const key = JSON.stringify(manifestOptions ?? {});
6114
- const cached = options.manifestCache.get(key);
6115
- if (cached) return cloneDiffManifest(cached);
6116
- const manifest = projectDiffManifest(options.diffManifest, manifestOptions);
6117
- options.manifestCache.set(key, manifest);
6118
- return cloneDiffManifest(manifest);
6119
- },
6120
- async changedFiles() {
6121
- return options.diffManifest.files.map((file) => ({
6122
- path: file.path,
6123
- previousPath: file.previousPath,
6124
- status: file.status
6125
- }));
6126
- },
6127
- async currentHeadSha() {
6128
- return options.event.change.head.sha;
6129
- }
5705
+ async getRepository(repository) {
5706
+ const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
5707
+ return {
5708
+ id: value.id,
5709
+ name: value.name,
5710
+ projectId: value.project.id,
5711
+ project: value.project.name
5712
+ };
6130
5713
  },
6131
- platform: { id: options.event.platform.id },
6132
- command: options.commandInvocation ? {
6133
- name: options.commandInvocation.name,
6134
- line: options.commandInvocation.line,
6135
- arguments: { ...options.commandInvocation.arguments },
6136
- async reply(markdown) {
6137
- collectCommandResponse(options.output, markdown, options.taskName);
5714
+ async getRepositoryPermission(actor, projectId, repositoryId) {
5715
+ const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
5716
+ descriptor: z.string().min(1),
5717
+ isActive: z.boolean().optional(),
5718
+ isContainer: z.boolean().optional(),
5719
+ memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
5720
+ })))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
5721
+ if (candidates.length !== 1) return "none";
5722
+ const identity = candidates[0];
5723
+ if (!identity) return "none";
5724
+ const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
5725
+ const loadAcls = (token) => {
5726
+ const query = new URLSearchParams({
5727
+ token,
5728
+ descriptors: descriptors.join(","),
5729
+ includeExtendedInfo: "true",
5730
+ recurse: "false",
5731
+ "api-version": "7.1"
5732
+ });
5733
+ return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
5734
+ };
5735
+ const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
5736
+ return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
5737
+ },
5738
+ getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
5739
+ async listIterations(repositoryId, changeNumber) {
5740
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
5741
+ id: iteration.id,
5742
+ headSha: iteration.sourceRefCommit.commitId
5743
+ }));
5744
+ },
5745
+ async loadChange(options) {
5746
+ if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
5747
+ const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
5748
+ const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
5749
+ const headSha = pullRequest.lastMergeSourceCommit.commitId;
5750
+ const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
5751
+ if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
5752
+ const sourceRef = branchName(pullRequest.sourceRefName);
5753
+ const targetRef = branchName(pullRequest.targetRefName);
5754
+ return {
5755
+ repository: {
5756
+ slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
5757
+ url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
5758
+ },
5759
+ coordinates: {
5760
+ provider: "azure-devops",
5761
+ organization,
5762
+ project: pullRequest.repository.project.name,
5763
+ projectId: pullRequest.repository.project.id,
5764
+ repositoryId: pullRequest.repository.id
5765
+ },
5766
+ change: {
5767
+ number: pullRequest.pullRequestId,
5768
+ isDraft: pullRequest.isDraft,
5769
+ title: pullRequest.title,
5770
+ description: pullRequest.description ?? "",
5771
+ url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
5772
+ author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
5773
+ base: {
5774
+ sha: pullRequest.lastMergeTargetCommit.commitId,
5775
+ ref: targetRef
5776
+ },
5777
+ head: {
5778
+ sha: headSha,
5779
+ ref: sourceRef,
5780
+ ...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
5781
+ },
5782
+ ...pullRequest.forkSource ? { isFork: true } : {}
5783
+ },
5784
+ iterationId: iteration.id
5785
+ };
5786
+ },
5787
+ async listIterationChanges(repositoryId, changeNumber, iterationId) {
5788
+ const changes = [];
5789
+ let skip = 0;
5790
+ let top = 2e3;
5791
+ for (;;) {
5792
+ const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
5793
+ const page = await api.json(withApiVersion(path), z.looseObject({
5794
+ changeEntries: z.array(iterationChangeSchema),
5795
+ nextSkip: z.number().int().nonnegative().optional(),
5796
+ nextTop: z.number().int().nonnegative().max(2e3).optional()
5797
+ }));
5798
+ changes.push(...page.changeEntries.map((change) => ({
5799
+ changeTrackingId: change.changeTrackingId,
5800
+ changeType: change.changeType,
5801
+ path: trimLeadingSlash(change.item.path),
5802
+ ...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
5803
+ })));
5804
+ if (!page.nextSkip) return changes;
5805
+ skip = page.nextSkip;
5806
+ if (page.nextTop) top = page.nextTop;
6138
5807
  }
6139
- } : void 0,
6140
- secret(secret) {
6141
- return resolveTaskSecret(secret, options);
6142
5808
  },
6143
- pi: { async run(agent, input, runOptions) {
6144
- const result = await runReviewAgent({
6145
- agent,
6146
- input,
6147
- runOptions,
6148
- runtime: {
6149
- ...options,
6150
- taskContext,
6151
- runId: options.runId,
6152
- piRunSink: options.piRunSink
6153
- }
6154
- });
6155
- options.output.providerModels.push(...result.providerModels);
6156
- if (result.repairAttempted) options.output.repairAttempted = true;
6157
- trackResultFindingScope(options.output, result.value, runOptions?.paths);
6158
- return agentOutputForTaskContext(agent, result.value);
6159
- } },
6160
- review: { async prior() {
6161
- return priorReviewForTask(options.priorMainComment, options.priorReviewState);
6162
- } },
6163
- check: createCheckHandle(options.output),
6164
- async comment(value) {
6165
- collectComment(options.output, value, options.taskName);
5809
+ async listThreads(repositoryId, changeNumber) {
5810
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
6166
5811
  },
6167
- log: options.taskLog ?? console
6168
- };
6169
- return taskContext;
6170
- }
6171
- function agentOutputForTaskContext(_agent, value) {
6172
- return value;
6173
- }
6174
- function resolveTaskSecret(secret, options) {
6175
- if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
6176
- const value = (options.env ?? process.env)[secret.name];
6177
- if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
6178
- options.log?.addSecret(value);
6179
- options.secretRedactor?.addSecret(value);
6180
- return value;
6181
- }
6182
- function commandResponseRuntimeResult(options) {
6183
- const redacted = redactCommandPublication({
6184
- body: options.commandResponse.value,
6185
- taskChecks: options.taskChecks,
6186
- redactor: options.secretRedactor
6187
- });
6188
- return {
6189
- kind: "command-response",
6190
- provider: options.provider,
6191
- diffManifest: options.diffManifest,
6192
- taskChecks: redacted.taskChecks,
6193
- repairAttempted: options.output.repairAttempted,
6194
- commandResponse: {
6195
- commandName: options.commandInvocation.name,
6196
- line: options.commandInvocation.line,
6197
- arguments: options.commandInvocation.arguments,
6198
- body: redacted.body
6199
- }
6200
- };
6201
- }
6202
- function publishTaskChecks(sink, checks) {
6203
- for (const check of checks) sink?.setTaskResult(check);
6204
- }
6205
- function skippedTaskRuntimeResult(options) {
6206
- const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
6207
- const review = {
6208
- summary: { body: reason },
6209
- inlineFindings: []
6210
- };
6211
- const validated = {
6212
- review,
6213
- validFindings: [],
6214
- droppedFindings: []
6215
- };
6216
- const publicationPlan = buildCommentPublishingPlan({
6217
- event: options.event,
6218
- main: reason,
6219
- validated,
6220
- manifest: options.diffManifest,
6221
- maxInlineComments: options.config.publication.maxInlineComments,
6222
- maxStoredFindings: options.config.publication.maxStoredFindings,
6223
- showHeader: options.config.publication.showHeader,
6224
- showFooter: options.config.publication.showFooter,
6225
- showStats: options.config.publication.showStats,
6226
- metadata: {
6227
- runtimeVersion,
6228
- configVersion: options.versionCompatibility?.configVersion,
6229
- trustedConfigSha: options.trustedConfigSha,
6230
- trustedConfigHash: options.trustedConfigHash,
6231
- reviewedHeadSha: options.event.change.head.sha,
6232
- providerModels: [options.provider.model],
6233
- selectedTasks: [],
6234
- failedTasks: [],
6235
- validFindings: 0,
6236
- droppedFindings: 0
6237
- }
6238
- }).publicationPlan;
6239
- return {
6240
- kind: "skipped",
6241
- skipReason: reason,
6242
- provider: options.provider,
6243
- diffManifest: options.diffManifest,
6244
- review,
6245
- validated,
6246
- publicationPlan,
6247
- mainComment: publicationPlan.mainComment,
6248
- inlineCommentDrafts: [],
6249
- taskChecks: [],
6250
- repairAttempted: false
5812
+ createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
5813
+ updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
5814
+ createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
5815
+ updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
5816
+ async createStatus(repositoryId, changeNumber, body) {
5817
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
5818
+ }
6251
5819
  };
6252
5820
  }
6253
- //#endregion
6254
- //#region src/shared/secret-redactor-core.ts
6255
- const redactedSecret = "[redacted secret]";
6256
- const minimumSecretLength = 4;
6257
- function createSecretRedactor(initialSecrets) {
6258
- const secrets = new Set(initialSecrets.filter((value) => value.length >= minimumSecretLength));
5821
+ function azureDevOpsStatusState(state) {
5822
+ switch (state) {
5823
+ case "pending": return "pending";
5824
+ case "success": return "succeeded";
5825
+ case "failure": return "failed";
5826
+ case "neutral": return "notApplicable";
5827
+ }
5828
+ }
5829
+ function azureRepositoryPermission(bits) {
5830
+ if ((bits & 2) === 0) return "none";
5831
+ if ((bits & 8192) !== 0) return "admin";
5832
+ if ((bits & 2048) !== 0) return "maintain";
5833
+ if ((bits & 4) !== 0) return "write";
5834
+ if ((bits & 16384) !== 0) return "triage";
5835
+ return "read";
5836
+ }
5837
+ function azurePermissionFromAcls(acls) {
5838
+ let allow = 0;
5839
+ let deny = 0;
5840
+ for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
5841
+ allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
5842
+ deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
5843
+ }
5844
+ return azureRepositoryPermission(allow & ~deny);
5845
+ }
5846
+ function collectionSchema(item) {
5847
+ return z.looseObject({
5848
+ count: z.number().int().nonnegative(),
5849
+ value: z.array(item)
5850
+ });
5851
+ }
5852
+ function withApiVersion(path) {
5853
+ return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
5854
+ }
5855
+ function jsonRequest$2(method, body) {
6259
5856
  return {
6260
- addSecret(value) {
6261
- if (value && value.length >= minimumSecretLength) secrets.add(value);
6262
- },
6263
- redact(value) {
6264
- const ordered = [...secrets].sort((left, right) => right.length - left.length);
6265
- let next = value;
6266
- for (const secret of ordered) next = next.replaceAll(secret, redactedSecret);
6267
- return {
6268
- value: next,
6269
- detected: next !== value
6270
- };
6271
- }
5857
+ method,
5858
+ headers: { "Content-Type": "application/json" },
5859
+ body: JSON.stringify(body)
6272
5860
  };
6273
5861
  }
6274
- //#endregion
6275
- //#region src/shared/secret-redactor.ts
6276
- function createKnownSecretRedactor(options) {
6277
- return createSecretRedactor(sensitiveEnvironmentValues(options?.env ?? process.env));
5862
+ function organizationFromCollectionUri$1(value) {
5863
+ return value ? azureOrganizationFromUrl(value) : void 0;
5864
+ }
5865
+ function branchName(ref) {
5866
+ return ref.replace(/^refs\/heads\//, "");
5867
+ }
5868
+ function repositoryWebUrl(organization, project, repository) {
5869
+ return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
5870
+ }
5871
+ function trimLeadingSlash(value) {
5872
+ return value.replace(/^\/+/, "");
6278
5873
  }
6279
5874
  //#endregion
6280
5875
  //#region src/hosts/env.ts
@@ -7013,17 +6608,228 @@ function createAzureDevOpsHostAdapter(options = {}) {
7013
6608
  }
7014
6609
  };
7015
6610
  }
7016
- function azureCoordinates(change) {
7017
- if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
7018
- return change.coordinates;
6611
+ function azureCoordinates(change) {
6612
+ if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
6613
+ return change.coordinates;
6614
+ }
6615
+ function azureCoordinatesFromRepository(client, slug) {
6616
+ const repositoryId = slug.split("/").at(-1);
6617
+ if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
6618
+ return {
6619
+ organization: client.organization,
6620
+ project: client.project,
6621
+ repositoryId
6622
+ };
6623
+ }
6624
+ //#endregion
6625
+ //#region src/hosts/bitbucket/schema.ts
6626
+ const bitbucketRepositorySchema = z.looseObject({
6627
+ uuid: z.string().min(1),
6628
+ name: z.string().min(1),
6629
+ full_name: z.string().min(1),
6630
+ slug: z.string().min(1).optional(),
6631
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
6632
+ }).transform((repository) => ({
6633
+ ...repository,
6634
+ slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
6635
+ }));
6636
+ //#endregion
6637
+ //#region src/hosts/bitbucket/client.ts
6638
+ const userSchema$1 = z.looseObject({
6639
+ uuid: z.string().optional(),
6640
+ nickname: z.string().optional(),
6641
+ display_name: z.string().optional()
6642
+ });
6643
+ const endpointSchema = z.looseObject({
6644
+ branch: z.looseObject({ name: z.string().min(1) }),
6645
+ commit: z.looseObject({ hash: z.string().min(1) }),
6646
+ repository: bitbucketRepositorySchema
6647
+ });
6648
+ const pullRequestSchema = z.looseObject({
6649
+ id: z.number().int().positive(),
6650
+ draft: z.boolean().optional(),
6651
+ title: z.string(),
6652
+ description: z.string().default(""),
6653
+ author: userSchema$1.optional(),
6654
+ source: endpointSchema,
6655
+ destination: endpointSchema,
6656
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
6657
+ });
6658
+ const inlineSchema = z.looseObject({
6659
+ path: z.string().optional(),
6660
+ from: z.number().int().nullable().optional(),
6661
+ to: z.number().int().nullable().optional(),
6662
+ start_from: z.number().int().nullable().optional(),
6663
+ start_to: z.number().int().nullable().optional()
6664
+ });
6665
+ const commentSchema = z.looseObject({
6666
+ id: z.union([z.number(), z.string()]).transform(String),
6667
+ content: z.looseObject({ raw: z.string().default("") }),
6668
+ user: userSchema$1.optional(),
6669
+ parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
6670
+ inline: inlineSchema.optional(),
6671
+ deleted: z.boolean().optional(),
6672
+ resolution: z.looseObject({}).optional()
6673
+ });
6674
+ function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
6675
+ const workspace = env.BITBUCKET_WORKSPACE;
6676
+ const repository = env.BITBUCKET_REPO_SLUG;
6677
+ const token = env.BITBUCKET_API_TOKEN;
6678
+ const email = env.BITBUCKET_EMAIL;
6679
+ if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
6680
+ if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
6681
+ if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
6682
+ const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
6683
+ const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
6684
+ const api = createCodeHostHttpClient({
6685
+ baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
6686
+ headers: { Authorization: authorization },
6687
+ fetch
6688
+ });
6689
+ const rootApi = createCodeHostHttpClient({
6690
+ baseUrl: "https://api.bitbucket.org/2.0/",
6691
+ headers: { Authorization: authorization },
6692
+ fetch
6693
+ });
6694
+ const prPath = (id) => `pullrequests/${id}`;
6695
+ return {
6696
+ workspace,
6697
+ repository,
6698
+ async currentUser() {
6699
+ const value = await rootApi.json("user", userSchema$1);
6700
+ return {
6701
+ uuid: value.uuid,
6702
+ nickname: value.nickname,
6703
+ displayName: value.display_name
6704
+ };
6705
+ },
6706
+ async getRepository() {
6707
+ const value = await api.json("", bitbucketRepositorySchema);
6708
+ return {
6709
+ uuid: value.uuid,
6710
+ slug: value.slug,
6711
+ fullName: value.full_name,
6712
+ url: value.links.html.href
6713
+ };
6714
+ },
6715
+ async getRepositoryPermission(actor, repositoryUuid) {
6716
+ const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
6717
+ const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
6718
+ if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
6719
+ const permissionApi = createCodeHostHttpClient({
6720
+ baseUrl: "https://api.bitbucket.org/2.0/",
6721
+ headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
6722
+ fetch
6723
+ });
6724
+ const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
6725
+ return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
6726
+ permission: z.enum([
6727
+ "read",
6728
+ "write",
6729
+ "admin"
6730
+ ]),
6731
+ user: userSchema$1
6732
+ })))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
6733
+ },
6734
+ getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
6735
+ async loadChange(options) {
6736
+ if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
6737
+ const pullRequest = await this.getPullRequest(options.changeNumber);
6738
+ return {
6739
+ repository: {
6740
+ slug: pullRequest.destination.repository.full_name,
6741
+ url: pullRequest.destination.repository.links.html.href
6742
+ },
6743
+ coordinates: {
6744
+ provider: "bitbucket",
6745
+ workspace,
6746
+ repository,
6747
+ repositoryUuid: pullRequest.destination.repository.uuid
6748
+ },
6749
+ change: {
6750
+ number: pullRequest.id,
6751
+ isDraft: pullRequest.draft,
6752
+ title: pullRequest.title,
6753
+ description: pullRequest.description,
6754
+ url: pullRequest.links.html.href,
6755
+ author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
6756
+ base: {
6757
+ sha: pullRequest.destination.commit.hash,
6758
+ ref: pullRequest.destination.branch.name,
6759
+ url: pullRequest.destination.repository.links.html.href
6760
+ },
6761
+ head: {
6762
+ sha: pullRequest.source.commit.hash,
6763
+ ref: pullRequest.source.branch.name,
6764
+ url: pullRequest.source.repository.links.html.href
6765
+ },
6766
+ isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
6767
+ }
6768
+ };
6769
+ },
6770
+ async listComments(id) {
6771
+ return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
6772
+ },
6773
+ createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
6774
+ updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
6775
+ async replyToComment(id, commentId, content) {
6776
+ const parentId = positiveCommentId(commentId);
6777
+ return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
6778
+ content: { raw: content },
6779
+ parent: { id: parentId }
6780
+ }));
6781
+ },
6782
+ async resolveComment(id, commentId) {
6783
+ positiveCommentId(commentId);
6784
+ await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
6785
+ },
6786
+ async setStatus(sha, key, body) {
6787
+ return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
6788
+ ...body,
6789
+ key
6790
+ }))).key;
6791
+ }
6792
+ };
6793
+ }
6794
+ function positiveCommentId(value) {
6795
+ const id = Number(value);
6796
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
6797
+ return id;
7019
6798
  }
7020
- function azureCoordinatesFromRepository(client, slug) {
7021
- const repositoryId = slug.split("/").at(-1);
7022
- if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
6799
+ function bitbucketStatusState(state) {
6800
+ if (state === "pending") return "INPROGRESS";
6801
+ if (state === "failure") return "FAILED";
6802
+ if (state === "neutral") return "STOPPED";
6803
+ return "SUCCESSFUL";
6804
+ }
6805
+ function escapeBitbucketQueryValue(value) {
6806
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
6807
+ }
6808
+ function pagedSchema(item) {
6809
+ return z.looseObject({
6810
+ values: z.array(item),
6811
+ next: z.string().url().optional()
6812
+ });
6813
+ }
6814
+ async function listAll(api, path, schema, allowedPathPrefix) {
6815
+ const values = [];
6816
+ let next = path;
6817
+ while (next) {
6818
+ const page = await api.json(next, pagedSchema(schema));
6819
+ values.push(...page.values);
6820
+ if (page.next) {
6821
+ const url = new URL(page.next);
6822
+ if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
6823
+ }
6824
+ next = page.next;
6825
+ }
6826
+ return values;
6827
+ }
6828
+ function jsonRequest$1(method, body) {
7023
6829
  return {
7024
- organization: client.organization,
7025
- project: client.project,
7026
- repositoryId
6830
+ method,
6831
+ headers: { "Content-Type": "application/json" },
6832
+ body: JSON.stringify(body)
7027
6833
  };
7028
6834
  }
7029
6835
  //#endregion
@@ -8740,6 +8546,213 @@ function createGitHubHostAdapter(options = {}) {
8740
8546
  };
8741
8547
  }
8742
8548
  //#endregion
8549
+ //#region src/hosts/gitlab/client.ts
8550
+ const userSchema = z.looseObject({
8551
+ id: z.number().int().positive(),
8552
+ username: z.string().min(1)
8553
+ });
8554
+ const noteSchema = z.looseObject({
8555
+ id: z.union([z.number(), z.string()]).transform(String),
8556
+ body: z.string(),
8557
+ author: userSchema.optional()
8558
+ });
8559
+ const positionSchema = z.looseObject({
8560
+ new_path: z.string().optional(),
8561
+ old_path: z.string().optional(),
8562
+ new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
8563
+ old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
8564
+ line_range: z.looseObject({
8565
+ start: z.looseObject({
8566
+ new_line: z.number().int().positive().nullish(),
8567
+ old_line: z.number().int().positive().nullish()
8568
+ }),
8569
+ end: z.looseObject({
8570
+ new_line: z.number().int().positive().nullish(),
8571
+ old_line: z.number().int().positive().nullish()
8572
+ })
8573
+ }).optional()
8574
+ });
8575
+ const discussionNoteSchema = noteSchema.extend({
8576
+ resolvable: z.boolean().optional(),
8577
+ resolved: z.boolean().optional(),
8578
+ position: positionSchema.nullable().optional()
8579
+ });
8580
+ const discussionSchema = z.looseObject({
8581
+ id: z.string().min(1),
8582
+ individual_note: z.boolean().optional(),
8583
+ notes: z.array(discussionNoteSchema)
8584
+ });
8585
+ const diffRefsSchema = z.looseObject({
8586
+ base_sha: z.string().min(1),
8587
+ start_sha: z.string().min(1),
8588
+ head_sha: z.string().min(1)
8589
+ });
8590
+ const mergeRequestResponseSchema = z.looseObject({
8591
+ iid: z.number().int().positive(),
8592
+ title: z.string().default(""),
8593
+ description: z.string().nullable().optional(),
8594
+ web_url: z.string().url().optional(),
8595
+ source_branch: z.string().min(1),
8596
+ target_branch: z.string().min(1),
8597
+ source_project_id: z.number().int().positive(),
8598
+ target_project_id: z.number().int().positive(),
8599
+ sha: z.string().min(1),
8600
+ draft: z.boolean().optional(),
8601
+ author: userSchema.optional(),
8602
+ references: z.looseObject({ full: z.string().optional() }).optional(),
8603
+ diff_refs: diffRefsSchema.nullable().optional()
8604
+ });
8605
+ const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
8606
+ const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
8607
+ const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
8608
+ function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
8609
+ const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
8610
+ if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
8611
+ const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
8612
+ const clientOptions = {
8613
+ baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
8614
+ headers,
8615
+ fetch,
8616
+ sleep: async (milliseconds) => void await sleep(milliseconds)
8617
+ };
8618
+ const api = createCodeHostHttpClient(clientOptions);
8619
+ const statusApi = createCodeHostHttpClient({
8620
+ ...clientOptions,
8621
+ retryNonIdempotentStatuses: [409]
8622
+ });
8623
+ return {
8624
+ async getProject(project) {
8625
+ const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
8626
+ id: z.union([z.number(), z.string()]).transform(String),
8627
+ path_with_namespace: z.string().min(1)
8628
+ }));
8629
+ return {
8630
+ id: value.id,
8631
+ path: value.path_with_namespace
8632
+ };
8633
+ },
8634
+ currentUser: () => api.json("user", userSchema),
8635
+ async loadChange(options) {
8636
+ const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
8637
+ return {
8638
+ repository: {
8639
+ slug: options.projectPath,
8640
+ url: projectWebUrl(mergeRequest.web_url)
8641
+ },
8642
+ coordinates: {
8643
+ provider: "gitlab",
8644
+ projectId: String(mergeRequest.target_project_id),
8645
+ projectPath: options.projectPath
8646
+ },
8647
+ change: {
8648
+ number: mergeRequest.iid,
8649
+ title: mergeRequest.title,
8650
+ description: mergeRequest.description ?? "",
8651
+ url: mergeRequest.web_url,
8652
+ author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
8653
+ base: {
8654
+ sha: mergeRequest.diff_refs.start_sha,
8655
+ ref: mergeRequest.target_branch
8656
+ },
8657
+ head: {
8658
+ sha: mergeRequest.diff_refs.head_sha,
8659
+ ref: mergeRequest.source_branch
8660
+ },
8661
+ isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
8662
+ isDraft: mergeRequest.draft
8663
+ }
8664
+ };
8665
+ },
8666
+ async getMergeRequest(projectId, changeNumber) {
8667
+ const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
8668
+ for (let attempt = 0;; attempt += 1) {
8669
+ const response = await api.json(requestPath, mergeRequestResponseSchema);
8670
+ const prepared = mergeRequestSchema.safeParse(response);
8671
+ if (prepared.success) return prepared.data;
8672
+ if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
8673
+ await sleep(250 * 2 ** attempt);
8674
+ }
8675
+ },
8676
+ async getRepositoryPermission(projectId, username) {
8677
+ const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
8678
+ if (!user) return "none";
8679
+ try {
8680
+ return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
8681
+ } catch (error) {
8682
+ if (error instanceof CodeHostHttpError && error.status === 404) return "none";
8683
+ throw error;
8684
+ }
8685
+ },
8686
+ listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
8687
+ createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
8688
+ updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
8689
+ listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
8690
+ getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
8691
+ async findReplyParent(projectId, changeNumber, noteId, discussionId) {
8692
+ if (!discussionId) return void 0;
8693
+ const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
8694
+ if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
8695
+ return discussion.notes[0]?.id;
8696
+ },
8697
+ createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
8698
+ body,
8699
+ position
8700
+ })),
8701
+ replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
8702
+ async resolveDiscussion(projectId, changeNumber, discussionId) {
8703
+ await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
8704
+ },
8705
+ async setStatus(projectId, sha, name, state, description) {
8706
+ const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
8707
+ state: gitLabStatusState(state),
8708
+ name,
8709
+ description: boundedStatusDescription(description)
8710
+ }));
8711
+ return String(status.id ?? name);
8712
+ }
8713
+ };
8714
+ }
8715
+ const MAX_PAGINATION_PAGES = 100;
8716
+ async function paginated(client, path, schema) {
8717
+ const values = [];
8718
+ for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
8719
+ const separator = path.includes("?") ? "&" : "?";
8720
+ const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
8721
+ values.push(...batch);
8722
+ if (batch.length < 100) return values;
8723
+ }
8724
+ throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
8725
+ }
8726
+ function jsonRequest(method, body) {
8727
+ return {
8728
+ method,
8729
+ headers: { "Content-Type": "application/json" },
8730
+ body: JSON.stringify(body)
8731
+ };
8732
+ }
8733
+ function accessLevelPermission(level) {
8734
+ if (level >= 50) return "admin";
8735
+ if (level >= 40) return "maintain";
8736
+ if (level >= 30) return "write";
8737
+ if (level >= 15) return "triage";
8738
+ if (level >= 10) return "read";
8739
+ return "none";
8740
+ }
8741
+ function boundedStatusDescription(description) {
8742
+ return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
8743
+ }
8744
+ function gitLabStatusState(state) {
8745
+ switch (state) {
8746
+ case "pending": return "pending";
8747
+ case "success": return "success";
8748
+ case "failure": return "failed";
8749
+ case "neutral": return "skipped";
8750
+ }
8751
+ }
8752
+ function projectWebUrl(mergeRequestUrl) {
8753
+ return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
8754
+ }
8755
+ //#endregion
8743
8756
  //#region src/hosts/gitlab/event.ts
8744
8757
  const projectSchema = z.looseObject({
8745
8758
  id: z.union([z.number(), z.string()]),
@@ -10252,6 +10265,6 @@ async function runHostRunCommandWithDependencies(options) {
10252
10265
  });
10253
10266
  }
10254
10267
  //#endregion
10255
- export { piReadOnlyToolNames as _, runInspectCommand as a, createGitHubHostAdapter as c, createGitLabClient as d, createBitbucketClient as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, PublicationError as l, azureOrganizationFromUrl as m, runHostRunCommand as n, runLocalReviewCommand as o, createAzureDevOpsClient as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createKnownSecretRedactor as u, piRequiredCliFlags as v, piThinkingLevels as y };
10268
+ export { piReadOnlyToolNames as _, runInspectCommand as a, createGitLabClient as c, PublicationError as d, createAzureDevOpsClient as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, createGitHubHostAdapter as l, createKnownSecretRedactor as m, runHostRunCommand as n, runLocalReviewCommand as o, azureOrganizationFromUrl as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createBitbucketClient as u, piRequiredCliFlags as v, piThinkingLevels as y };
10256
10269
 
10257
- //# sourceMappingURL=commands-EZV-Icbs.mjs.map
10270
+ //# sourceMappingURL=commands-B9qNW4pk.mjs.map