@usepipr/runtime 0.4.1 → 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.
- package/dist/{commands-46Q2ZDt0.mjs → commands-B9qNW4pk.mjs} +1667 -1382
- package/dist/commands-B9qNW4pk.mjs.map +1 -0
- package/dist/commands-DA0Ij4Di.d.mts +27 -0
- package/dist/commands-DA0Ij4Di.d.mts.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/internal/action-result.d.mts +11 -0
- package/dist/internal/action-result.d.mts.map +1 -0
- package/dist/internal/action-result.mjs +46 -0
- package/dist/internal/action-result.mjs.map +1 -0
- package/dist/internal/docs.mjs +1 -1
- package/dist/internal/testing.d.mts +2 -1
- package/dist/internal/testing.d.mts.map +1 -1
- package/dist/internal/testing.mjs +1 -1
- package/dist/{official-github-workflow-Cx-Yhh86.mjs → official-github-workflow-Bjlu-FL3.mjs} +9 -11
- package/dist/{official-github-workflow-Cx-Yhh86.mjs.map → official-github-workflow-Bjlu-FL3.mjs.map} +1 -1
- package/dist/{commands-0cyQHxpm.d.mts → types-BAis_Lwm.d.mts} +10 -30
- package/dist/types-BAis_Lwm.d.mts.map +1 -0
- package/package.json +7 -3
- package/dist/commands-0cyQHxpm.d.mts.map +0 -1
- package/dist/commands-46Q2ZDt0.mjs.map +0 -1
|
@@ -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-
|
|
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.
|
|
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.
|
|
943
|
-
const defaultSdkVersion = "0.4.
|
|
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
|
|
1090
|
-
return
|
|
1090
|
+
function officialInitPackageManifest(env = process.env) {
|
|
1091
|
+
return {
|
|
1091
1092
|
private: true,
|
|
1092
|
-
dependencies: { "@usepipr/sdk":
|
|
1093
|
+
dependencies: { "@usepipr/sdk": env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
|
|
1093
1094
|
devDependencies: {
|
|
1094
|
-
"@types/bun":
|
|
1095
|
-
typescript:
|
|
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
|
-
}
|
|
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/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
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
|
|
1236
|
-
|
|
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
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
});
|
|
@@ -3837,6 +3021,146 @@ function findingFingerprint(finding) {
|
|
|
3837
3021
|
return new Bun.CryptoHasher("sha256").update(basis).digest("hex").slice(0, 16);
|
|
3838
3022
|
}
|
|
3839
3023
|
//#endregion
|
|
3024
|
+
//#region src/review/agent/review-schema.ts
|
|
3025
|
+
const reviewFindingPropertyNames = [
|
|
3026
|
+
"body",
|
|
3027
|
+
"path",
|
|
3028
|
+
"rangeId",
|
|
3029
|
+
"side",
|
|
3030
|
+
"startLine",
|
|
3031
|
+
"endLine"
|
|
3032
|
+
];
|
|
3033
|
+
const completeReviewFindingPropertyMask = (1 << reviewFindingPropertyNames.length) - 1;
|
|
3034
|
+
const unresolvedSchemaReference = Symbol("unresolvedSchemaReference");
|
|
3035
|
+
const singleSchemaKeywords = [
|
|
3036
|
+
"additionalItems",
|
|
3037
|
+
"additionalProperties",
|
|
3038
|
+
"contains",
|
|
3039
|
+
"else",
|
|
3040
|
+
"if",
|
|
3041
|
+
"items",
|
|
3042
|
+
"then",
|
|
3043
|
+
"unevaluatedProperties"
|
|
3044
|
+
];
|
|
3045
|
+
const schemaArrayKeywords = [
|
|
3046
|
+
"allOf",
|
|
3047
|
+
"anyOf",
|
|
3048
|
+
"oneOf",
|
|
3049
|
+
"prefixItems"
|
|
3050
|
+
];
|
|
3051
|
+
const schemaMapKeywords = [
|
|
3052
|
+
"dependentSchemas",
|
|
3053
|
+
"patternProperties",
|
|
3054
|
+
"properties"
|
|
3055
|
+
];
|
|
3056
|
+
const alternativeSchemaKeywords = ["anyOf", "oneOf"];
|
|
3057
|
+
function schemaContainsReviewFinding(value) {
|
|
3058
|
+
if (!isRecord(value)) return false;
|
|
3059
|
+
return reachableSchemaContainsReviewFinding(value, value, /* @__PURE__ */ new Set());
|
|
3060
|
+
}
|
|
3061
|
+
function reachableSchemaContainsReviewFinding(schema, rootSchema, visited) {
|
|
3062
|
+
if (Array.isArray(schema)) return schema.some((child) => reachableSchemaContainsReviewFinding(child, rootSchema, visited));
|
|
3063
|
+
if (!isRecord(schema) || visited.has(schema)) return false;
|
|
3064
|
+
visited.add(schema);
|
|
3065
|
+
return schemaObjectPropertyMasks(schema, rootSchema, /* @__PURE__ */ new Set()).has(completeReviewFindingPropertyMask) || reachableChildSchemas(schema, rootSchema).some((child) => reachableSchemaContainsReviewFinding(child, rootSchema, visited));
|
|
3066
|
+
}
|
|
3067
|
+
function reachableChildSchemas(schema, rootSchema) {
|
|
3068
|
+
const referencedSchema = resolveLocalSchemaReference(rootSchema, schema.$ref);
|
|
3069
|
+
return [
|
|
3070
|
+
...referencedSchema === void 0 ? [] : [referencedSchema],
|
|
3071
|
+
...singleSchemaKeywords.map((keyword) => schema[keyword]),
|
|
3072
|
+
...schemaArrayKeywords.flatMap((keyword) => Array.isArray(schema[keyword]) ? schema[keyword] : []),
|
|
3073
|
+
...schemaMapKeywords.flatMap((keyword) => {
|
|
3074
|
+
const children = schema[keyword];
|
|
3075
|
+
return isRecord(children) ? Object.values(children) : [];
|
|
3076
|
+
})
|
|
3077
|
+
];
|
|
3078
|
+
}
|
|
3079
|
+
function schemaObjectPropertyMasks(schema, rootSchema, visited) {
|
|
3080
|
+
if (schema === true || visited.has(schema)) return /* @__PURE__ */ new Set([0]);
|
|
3081
|
+
if (!isRecord(schema) || !schemaAllowsObject(schema, rootSchema, /* @__PURE__ */ new Set())) return /* @__PURE__ */ new Set();
|
|
3082
|
+
visited.add(schema);
|
|
3083
|
+
const conjunctiveMasks = conjunctiveSchemas(schema, rootSchema).reduce((masks, child) => combinePropertyMasks(masks, schemaObjectPropertyMasks(child, rootSchema, new Set(visited))), /* @__PURE__ */ new Set([reviewFindingPropertyMask(schema)]));
|
|
3084
|
+
return alternativeSchemaGroups(schema).reduce((masks, alternatives) => combinePropertyMasks(masks, alternativePropertyMasks(alternatives, rootSchema, visited)), conjunctiveMasks);
|
|
3085
|
+
}
|
|
3086
|
+
function conjunctiveSchemas(schema, rootSchema) {
|
|
3087
|
+
const referencedSchema = resolveLocalSchemaReference(rootSchema, schema.$ref);
|
|
3088
|
+
const allOf = Array.isArray(schema.allOf) ? schema.allOf : [];
|
|
3089
|
+
return referencedSchema === void 0 ? allOf : [referencedSchema, ...allOf];
|
|
3090
|
+
}
|
|
3091
|
+
function alternativeSchemaGroups(schema) {
|
|
3092
|
+
return alternativeSchemaKeywords.map((keyword) => schema[keyword]).filter((alternatives) => Array.isArray(alternatives));
|
|
3093
|
+
}
|
|
3094
|
+
function alternativePropertyMasks(alternatives, rootSchema, visited) {
|
|
3095
|
+
return new Set(alternatives.flatMap((child) => [...schemaObjectPropertyMasks(child, rootSchema, new Set(visited))]));
|
|
3096
|
+
}
|
|
3097
|
+
function reviewFindingPropertyMask(schema) {
|
|
3098
|
+
let mask = 0;
|
|
3099
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
3100
|
+
const patternProperties = isRecord(schema.patternProperties) ? schema.patternProperties : {};
|
|
3101
|
+
const propertyPatterns = Object.keys(patternProperties).flatMap((pattern) => {
|
|
3102
|
+
try {
|
|
3103
|
+
return [new RegExp(pattern)];
|
|
3104
|
+
} catch {
|
|
3105
|
+
return [];
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required.filter((propertyName) => typeof propertyName === "string") : []);
|
|
3109
|
+
for (const [index, propertyName] of reviewFindingPropertyNames.entries()) if (propertyName in properties || required.has(propertyName) || propertyPatterns.some((pattern) => pattern.test(propertyName))) mask |= 1 << index;
|
|
3110
|
+
return mask;
|
|
3111
|
+
}
|
|
3112
|
+
function combinePropertyMasks(left, right) {
|
|
3113
|
+
const combined = /* @__PURE__ */ new Set();
|
|
3114
|
+
for (const leftMask of left) for (const rightMask of right) {
|
|
3115
|
+
combined.add(leftMask | rightMask);
|
|
3116
|
+
if (combined.has(completeReviewFindingPropertyMask)) return /* @__PURE__ */ new Set([completeReviewFindingPropertyMask]);
|
|
3117
|
+
}
|
|
3118
|
+
return combined;
|
|
3119
|
+
}
|
|
3120
|
+
function schemaAllowsObject(schema, rootSchema, visited) {
|
|
3121
|
+
if (schema === false) return false;
|
|
3122
|
+
if (schema === true || !isRecord(schema) || visited.has(schema)) return true;
|
|
3123
|
+
visited.add(schema);
|
|
3124
|
+
return directSchemaAllowsObject(schema) && conjunctiveSchemas(schema, rootSchema).every((child) => schemaAllowsObject(child, rootSchema, new Set(visited))) && alternativeSchemaGroups(schema).every((alternatives) => alternatives.some((child) => schemaAllowsObject(child, rootSchema, new Set(visited))));
|
|
3125
|
+
}
|
|
3126
|
+
function directSchemaAllowsObject(schema) {
|
|
3127
|
+
const type = schema.type;
|
|
3128
|
+
if (typeof type === "string") return type === "object";
|
|
3129
|
+
if (Array.isArray(type) && !type.includes("object")) return false;
|
|
3130
|
+
if ("const" in schema && !isRecord(schema.const)) return false;
|
|
3131
|
+
return !Array.isArray(schema.enum) || schema.enum.some(isRecord);
|
|
3132
|
+
}
|
|
3133
|
+
function resolveLocalSchemaReference(rootSchema, reference) {
|
|
3134
|
+
if (reference === "#") return rootSchema;
|
|
3135
|
+
if (typeof reference !== "string" || !reference.startsWith("#/")) return;
|
|
3136
|
+
let current = rootSchema;
|
|
3137
|
+
for (const encodedSegment of reference.slice(2).split("/")) {
|
|
3138
|
+
const segment = decodePointerSegment(encodedSegment);
|
|
3139
|
+
if (segment === void 0) return;
|
|
3140
|
+
current = resolvePointerSegment(current, segment);
|
|
3141
|
+
if (current === unresolvedSchemaReference) return;
|
|
3142
|
+
}
|
|
3143
|
+
return current;
|
|
3144
|
+
}
|
|
3145
|
+
function decodePointerSegment(encodedSegment) {
|
|
3146
|
+
try {
|
|
3147
|
+
return decodeURIComponent(encodedSegment).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
3148
|
+
} catch {
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function resolvePointerSegment(current, segment) {
|
|
3153
|
+
if (Array.isArray(current)) return resolveArrayPointerSegment(current, segment);
|
|
3154
|
+
if (!isRecord(current) || !(segment in current)) return unresolvedSchemaReference;
|
|
3155
|
+
return current[segment];
|
|
3156
|
+
}
|
|
3157
|
+
function resolveArrayPointerSegment(current, segment) {
|
|
3158
|
+
if (!/^(0|[1-9]\d*)$/.test(segment)) return unresolvedSchemaReference;
|
|
3159
|
+
const index = Number(segment);
|
|
3160
|
+
if (!Number.isSafeInteger(index) || index >= current.length) return unresolvedSchemaReference;
|
|
3161
|
+
return current[index];
|
|
3162
|
+
}
|
|
3163
|
+
//#endregion
|
|
3840
3164
|
//#region src/review/agent/agent-prompt.ts
|
|
3841
3165
|
async function renderAgentPrompt(options) {
|
|
3842
3166
|
const prompt = await renderAgentDefinitionPrompt(options.agent, options.input, options.agentRunContext.prompt);
|
|
@@ -3893,12 +3217,12 @@ function outputPrompt(schema) {
|
|
|
3893
3217
|
]);
|
|
3894
3218
|
if (schema.id === reviewResultSchemaId) {
|
|
3895
3219
|
lines.splice(2, 0, `Example:\n${JSON.stringify(reviewSchemaExample$1(), null, 2)}`, ...suggestedFixRules);
|
|
3896
|
-
lines.push("For inlineFindings, use only fields shown in the schema. Each finding's path, rangeId, and side must identify one Diff Manifest commentable range, and its startLine and endLine must select a valid span within that range. If no valid span applies, omit the finding.", ...inlineSelectionPromptLines(),
|
|
3220
|
+
lines.push("For inlineFindings, use only fields shown in the schema. Each finding's path, rangeId, and side must identify one Diff Manifest commentable range, and its startLine and endLine must select a valid span within that range. If no valid span applies, omit the finding.", ...inlineSelectionPromptLines(), "For inlineFindings.body, write the exact inline comment body.");
|
|
3897
3221
|
} else if (schemaMentionsField(schema.jsonSchema, "suggestedFix")) lines.splice(2, 0, ...suggestedFixRules);
|
|
3898
3222
|
return lines.join("\n\n");
|
|
3899
3223
|
}
|
|
3900
3224
|
function customInlineSelectionPrompt(schema, diffManifest) {
|
|
3901
|
-
if (!diffManifest || schema.id === reviewResultSchemaId) return;
|
|
3225
|
+
if (!diffManifest || schema.id === reviewResultSchemaId || !schemaContainsReviewFinding(schema.jsonSchema)) return;
|
|
3902
3226
|
return promptSection("Inline Review Selection Policy", inlineSelectionPromptLines().join("\n"));
|
|
3903
3227
|
}
|
|
3904
3228
|
function inlineSelectionPromptLines() {
|
|
@@ -3922,16 +3246,16 @@ function schemaMentionsField(value, fieldName) {
|
|
|
3922
3246
|
return Object.entries(value).some(([key, child]) => key === fieldName || schemaMentionsField(child, fieldName));
|
|
3923
3247
|
}
|
|
3924
3248
|
function reviewPolicyPrompt(schema) {
|
|
3925
|
-
if (schema.id !== reviewResultSchemaId) return;
|
|
3249
|
+
if (schema.id !== reviewResultSchemaId && !schemaContainsReviewFinding(schema.jsonSchema)) return;
|
|
3926
3250
|
return promptSection("Review Policy", [
|
|
3927
3251
|
"Review only changed behavior.",
|
|
3928
3252
|
"Report only actionable defects, security risks, regressions, or meaningful test gaps.",
|
|
3929
3253
|
"Before emitting a finding, verify that the changed code introduces or exposes the issue, repository evidence supports it, and the impact is concrete. If any part is uncertain, omit it.",
|
|
3930
3254
|
"When changed behavior crosses a function, type, API, configuration, or data boundary, inspect relevant callers, callees, and tests before deciding whether the change is defective or intentionally coordinated.",
|
|
3931
|
-
"Put each actionable issue in
|
|
3932
|
-
"
|
|
3933
|
-
"
|
|
3934
|
-
`State the concrete defect and user-visible or runtime impact directly. Keep each body to one short paragraph, at most two sentences, and at most 700 characters.`,
|
|
3255
|
+
"Put each actionable issue in the schema's finding collection. Do not leave actionable defects or test gaps only in the summary.",
|
|
3256
|
+
"When the output includes a summary, base it only on changed behavior and evidence available in the Diff Manifest or read tools. Do not claim tests or checks ran, passed, or failed unless their output is present.",
|
|
3257
|
+
"Finding bodies must be publication-ready review prose, not analysis notes.",
|
|
3258
|
+
`State the concrete defect and user-visible or runtime impact directly. Keep each body to one short paragraph, at most two sentences, and at most 700 characters. Treat 700 as a hard ceiling, not a target; prefer 250-450 characters when possible.`,
|
|
3935
3259
|
"Do not include step-by-step reasoning, broad context, praise, restated diff, alternatives, or code snippets unless they are necessary to identify the defect.",
|
|
3936
3260
|
"Never copy a secret-looking literal from changed code into the review summary, inline finding body, or suggestedFix. Describe only the secret kind and location.",
|
|
3937
3261
|
"Omit speculative, style-only, broad refactor, external-fact, and out-of-diff findings.",
|
|
@@ -3950,14 +3274,15 @@ function pathScopePrompt(paths) {
|
|
|
3950
3274
|
].join("\n");
|
|
3951
3275
|
}
|
|
3952
3276
|
function priorFindingsPrompt(state) {
|
|
3953
|
-
const
|
|
3954
|
-
if (
|
|
3277
|
+
const findings = state?.findings.filter((finding) => finding.status === "open") ?? [];
|
|
3278
|
+
if (findings.length === 0) return;
|
|
3955
3279
|
return [
|
|
3956
3280
|
"Prior pipr findings:",
|
|
3957
3281
|
JSON.stringify({
|
|
3958
3282
|
reviewedHeadSha: state?.reviewedHeadSha,
|
|
3959
|
-
findings:
|
|
3283
|
+
findings: findings.map((finding) => ({
|
|
3960
3284
|
id: finding.id,
|
|
3285
|
+
status: finding.status,
|
|
3961
3286
|
path: finding.path,
|
|
3962
3287
|
rangeId: finding.rangeId,
|
|
3963
3288
|
side: finding.side,
|
|
@@ -4446,6 +3771,8 @@ const findingIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/);
|
|
|
4446
3771
|
const priorFindingStatusSchema = z.enum(["open", "resolved"]);
|
|
4447
3772
|
const priorFindingRecordSchema = z.strictObject({
|
|
4448
3773
|
id: findingIdSchema,
|
|
3774
|
+
anchorFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
3775
|
+
issueFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
4449
3776
|
status: priorFindingStatusSchema,
|
|
4450
3777
|
path: z.string().min(1),
|
|
4451
3778
|
rangeId: z.string().min(1),
|
|
@@ -4469,33 +3796,23 @@ function buildPriorReviewState(options) {
|
|
|
4469
3796
|
const nextFindings = /* @__PURE__ */ new Map();
|
|
4470
3797
|
const usedPriorIds = /* @__PURE__ */ new Set();
|
|
4471
3798
|
const stats = accumulateReviewStats(scopedPriorState?.stats, options.stats);
|
|
4472
|
-
|
|
4473
|
-
|
|
3799
|
+
const findings = options.findings.map((item) => item.finding);
|
|
3800
|
+
const fingerprintCounts = countFindingFingerprints(options.findings);
|
|
3801
|
+
for (const { finding, anchorFingerprint, issueFingerprint, previousPath } of options.findings) {
|
|
3802
|
+
const record = buildFindingRecord({
|
|
4474
3803
|
finding,
|
|
4475
|
-
findings
|
|
3804
|
+
findings,
|
|
4476
3805
|
priorFindings,
|
|
4477
|
-
usedPriorIds
|
|
3806
|
+
usedPriorIds,
|
|
3807
|
+
reviewedHeadSha: options.reviewedHeadSha,
|
|
3808
|
+
anchorFingerprint,
|
|
3809
|
+
issueFingerprint,
|
|
3810
|
+
previousPath,
|
|
3811
|
+
fingerprintCounts
|
|
4478
3812
|
});
|
|
4479
|
-
|
|
4480
|
-
if (prior) usedPriorIds.add(prior.id);
|
|
4481
|
-
const record = {
|
|
4482
|
-
id,
|
|
4483
|
-
status: "open",
|
|
4484
|
-
path: finding.path,
|
|
4485
|
-
rangeId: finding.rangeId,
|
|
4486
|
-
side: finding.side,
|
|
4487
|
-
startLine: finding.startLine,
|
|
4488
|
-
endLine: finding.endLine,
|
|
4489
|
-
firstSeenHeadSha: prior?.firstSeenHeadSha ?? options.reviewedHeadSha,
|
|
4490
|
-
lastSeenHeadSha: options.reviewedHeadSha,
|
|
4491
|
-
...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
|
|
4492
|
-
};
|
|
4493
|
-
nextFindings.set(id, record);
|
|
4494
|
-
}
|
|
4495
|
-
for (const prior of priorFindings.values()) {
|
|
4496
|
-
if (nextFindings.has(prior.id)) continue;
|
|
4497
|
-
nextFindings.set(prior.id, prior);
|
|
3813
|
+
nextFindings.set(record.id, record);
|
|
4498
3814
|
}
|
|
3815
|
+
addHistoricalFindings(nextFindings, priorFindings.values());
|
|
4499
3816
|
return {
|
|
4500
3817
|
version: 1,
|
|
4501
3818
|
reviewedHeadSha: options.reviewedHeadSha,
|
|
@@ -4504,6 +3821,57 @@ function buildPriorReviewState(options) {
|
|
|
4504
3821
|
...stats ? { stats } : {}
|
|
4505
3822
|
};
|
|
4506
3823
|
}
|
|
3824
|
+
function buildFindingRecord(options) {
|
|
3825
|
+
const selection = selectPriorFindingRecord(options);
|
|
3826
|
+
const prior = options.priorFindings.get(selection.id);
|
|
3827
|
+
markPriorFindingUsed(options.usedPriorIds, prior);
|
|
3828
|
+
return {
|
|
3829
|
+
id: selection.id,
|
|
3830
|
+
...findingIdentity(options.anchorFingerprint, options.issueFingerprint),
|
|
3831
|
+
status: selection.status,
|
|
3832
|
+
path: options.finding.path,
|
|
3833
|
+
rangeId: options.finding.rangeId,
|
|
3834
|
+
side: options.finding.side,
|
|
3835
|
+
startLine: options.finding.startLine,
|
|
3836
|
+
endLine: options.finding.endLine,
|
|
3837
|
+
...findingHistory(prior, options.reviewedHeadSha)
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
function selectPriorFindingRecord(options) {
|
|
3841
|
+
const resolvedMatch = matchResolvedFindingRecord([...options.priorFindings.values()], options.finding, options.anchorFingerprint, options.issueFingerprint, options.fingerprintCounts, options.previousPath);
|
|
3842
|
+
if (resolvedMatch) return {
|
|
3843
|
+
id: resolvedMatch.id,
|
|
3844
|
+
status: "resolved"
|
|
3845
|
+
};
|
|
3846
|
+
return {
|
|
3847
|
+
id: selectFindingId({
|
|
3848
|
+
finding: options.finding,
|
|
3849
|
+
findings: options.findings,
|
|
3850
|
+
priorFindings: options.priorFindings,
|
|
3851
|
+
usedPriorIds: options.usedPriorIds
|
|
3852
|
+
}),
|
|
3853
|
+
status: "open"
|
|
3854
|
+
};
|
|
3855
|
+
}
|
|
3856
|
+
function markPriorFindingUsed(usedPriorIds, prior) {
|
|
3857
|
+
if (prior) usedPriorIds.add(prior.id);
|
|
3858
|
+
}
|
|
3859
|
+
function findingIdentity(anchorFingerprint, issueFingerprint) {
|
|
3860
|
+
return {
|
|
3861
|
+
...anchorFingerprint ? { anchorFingerprint } : {},
|
|
3862
|
+
...issueFingerprint ? { issueFingerprint } : {}
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
function findingHistory(prior, reviewedHeadSha) {
|
|
3866
|
+
return {
|
|
3867
|
+
firstSeenHeadSha: prior?.firstSeenHeadSha ?? reviewedHeadSha,
|
|
3868
|
+
lastSeenHeadSha: reviewedHeadSha,
|
|
3869
|
+
...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
function addHistoricalFindings(findings, historicalFindings) {
|
|
3873
|
+
for (const finding of historicalFindings) if (!findings.has(finding.id)) findings.set(finding.id, finding);
|
|
3874
|
+
}
|
|
4507
3875
|
function resolvePriorFindings(state, findingIds) {
|
|
4508
3876
|
const resolved = new Set(findingIds);
|
|
4509
3877
|
return {
|
|
@@ -4523,6 +3891,22 @@ function matchFindingRecord(state, finding) {
|
|
|
4523
3891
|
if (deterministic) return deterministic;
|
|
4524
3892
|
return findOpenOverlappingFinding(state.findings, finding);
|
|
4525
3893
|
}
|
|
3894
|
+
function matchResolvedFindingRecord(records, finding, anchorFingerprint, issueFingerprint, currentFingerprintCounts, previousPath) {
|
|
3895
|
+
if (!anchorFingerprint || !issueFingerprint || (currentFingerprintCounts?.get(findingFingerprintKey(finding.path, anchorFingerprint, issueFingerprint)) ?? 1) !== 1) return;
|
|
3896
|
+
const candidates = records.filter((record) => record.anchorFingerprint === anchorFingerprint && record.issueFingerprint === issueFingerprint && (record.path === finding.path || record.path === previousPath));
|
|
3897
|
+
return candidates.length === 1 && candidates[0]?.status === "resolved" ? candidates[0] : void 0;
|
|
3898
|
+
}
|
|
3899
|
+
function countFindingFingerprints(findings) {
|
|
3900
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3901
|
+
for (const { finding, anchorFingerprint, issueFingerprint } of findings) if (anchorFingerprint && issueFingerprint) {
|
|
3902
|
+
const key = findingFingerprintKey(finding.path, anchorFingerprint, issueFingerprint);
|
|
3903
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3904
|
+
}
|
|
3905
|
+
return counts;
|
|
3906
|
+
}
|
|
3907
|
+
function findingFingerprintKey(path, anchorFingerprint, issueFingerprint) {
|
|
3908
|
+
return `${path}\0${anchorFingerprint}\0${issueFingerprint}`;
|
|
3909
|
+
}
|
|
4526
3910
|
function renderMainCommentMarker(options) {
|
|
4527
3911
|
return `<!-- ${options.marker} change=${options.changeNumber} version=1 state=${encodeReviewState({
|
|
4528
3912
|
...options.reviewState,
|
|
@@ -4587,6 +3971,20 @@ function applyResolvedFindingMarkers(state, commentBodies) {
|
|
|
4587
3971
|
}))
|
|
4588
3972
|
};
|
|
4589
3973
|
}
|
|
3974
|
+
function applyNativeThreadResolutions(state, resolutions) {
|
|
3975
|
+
const resolutionByFinding = new Map(resolutions.map((resolution) => [`${resolution.findingId}:${resolution.findingHeadSha}`, resolution.resolved]));
|
|
3976
|
+
return {
|
|
3977
|
+
...state,
|
|
3978
|
+
findings: state.findings.map((finding) => {
|
|
3979
|
+
if (!finding.lastCommentedHeadSha) return finding;
|
|
3980
|
+
const resolved = resolutionByFinding.get(`${finding.id}:${finding.lastCommentedHeadSha}`);
|
|
3981
|
+
return resolved === void 0 ? finding : {
|
|
3982
|
+
...finding,
|
|
3983
|
+
status: resolved ? "resolved" : "open"
|
|
3984
|
+
};
|
|
3985
|
+
})
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
4590
3988
|
function extractVerifierResponseMarkers(commentBodies) {
|
|
4591
3989
|
return new Set(extractMarkerRecords(commentBodies, verifierResponseMarkerPrefix).map((record) => record.marker));
|
|
4592
3990
|
}
|
|
@@ -4782,7 +4180,7 @@ function publicationPlanForHostCapabilities(plan, capabilities) {
|
|
|
4782
4180
|
}
|
|
4783
4181
|
function buildPublicationPlan(options) {
|
|
4784
4182
|
const reviewState = options.reviewState ?? buildPriorReviewState({
|
|
4785
|
-
findings: options.inlineItems.map((item) => item.finding),
|
|
4183
|
+
findings: options.inlineItems.map((item) => ({ finding: item.finding })),
|
|
4786
4184
|
reviewedHeadSha: options.metadata.reviewedHeadSha,
|
|
4787
4185
|
selectedTasks: options.metadata.selectedTasks
|
|
4788
4186
|
});
|
|
@@ -4827,10 +4225,12 @@ function preparePublishableInlineFindings(options) {
|
|
|
4827
4225
|
}
|
|
4828
4226
|
function prepareInlinePublicationItemsForPublishableFindings(options) {
|
|
4829
4227
|
const seenFindingIds = /* @__PURE__ */ new Set();
|
|
4830
|
-
|
|
4228
|
+
const fingerprintCounts = countFindingFingerprints(options.publishableFindings);
|
|
4229
|
+
return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath, anchorFingerprint, issueFingerprint }) => {
|
|
4831
4230
|
const findingId = findingIdFor(publishableFinding, options.reviewState);
|
|
4832
4231
|
const stateRecord = options.reviewState ? matchFindingRecord(options.reviewState, publishableFinding) : void 0;
|
|
4833
|
-
|
|
4232
|
+
const resolvedRecord = options.reviewState ? matchResolvedFindingRecord(options.reviewState.findings, publishableFinding, anchorFingerprint, issueFingerprint, fingerprintCounts, previousPath) : void 0;
|
|
4233
|
+
if (seenFindingIds.has(findingId) || resolvedRecord !== void 0 || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
|
|
4834
4234
|
seenFindingIds.add(findingId);
|
|
4835
4235
|
const marker = inlineFindingMarker(findingId, options.reviewedHeadSha);
|
|
4836
4236
|
return [inlinePublicationItemSchema.parse({
|
|
@@ -4977,10 +4377,17 @@ function buildCommentPublishingPlan(options) {
|
|
|
4977
4377
|
const publishableInlineFindings = preparePublishableInlineFindings({
|
|
4978
4378
|
validated: options.validated,
|
|
4979
4379
|
manifest: options.manifest
|
|
4380
|
+
}).map((item) => {
|
|
4381
|
+
const fingerprint = selectedCodeFingerprint(item.finding, item.range);
|
|
4382
|
+
return fingerprint ? {
|
|
4383
|
+
...item,
|
|
4384
|
+
anchorFingerprint: fingerprint,
|
|
4385
|
+
issueFingerprint: findingIssueFingerprint(item.finding)
|
|
4386
|
+
} : item;
|
|
4980
4387
|
});
|
|
4981
4388
|
const reviewState = buildPriorReviewState({
|
|
4982
4389
|
priorState: options.priorReviewState,
|
|
4983
|
-
findings: publishableInlineFindings
|
|
4390
|
+
findings: publishableInlineFindings,
|
|
4984
4391
|
reviewedHeadSha: options.event.change.head.sha,
|
|
4985
4392
|
selectedTasks: options.metadata.selectedTasks,
|
|
4986
4393
|
stats: options.metadata.stats
|
|
@@ -5011,6 +4418,20 @@ function buildCommentPublishingPlan(options) {
|
|
|
5011
4418
|
inlineCommentDrafts: publicationPlan.inlineItems
|
|
5012
4419
|
};
|
|
5013
4420
|
}
|
|
4421
|
+
function selectedCodeFingerprint(finding, range) {
|
|
4422
|
+
if (range.preview === void 0) return;
|
|
4423
|
+
const lines = range.preview.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
4424
|
+
const startOffset = finding.startLine - range.startLine;
|
|
4425
|
+
const endOffset = finding.endLine - range.startLine + 1;
|
|
4426
|
+
if (startOffset < 0 || endOffset > lines.length) return;
|
|
4427
|
+
const selected = lines.slice(startOffset, endOffset).map((line) => line.trimEnd()).join("\n");
|
|
4428
|
+
return new Bun.CryptoHasher("sha256").update(selected).digest("hex");
|
|
4429
|
+
}
|
|
4430
|
+
function findingIssueFingerprint(finding) {
|
|
4431
|
+
const identity = finding.body.normalize("NFKC").toLowerCase().replace(/<[^>]*>/g, " ").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[^\p{L}\p{N}_]+/gu, " ").trim().replace(/\s+/g, " ") || finding.body.normalize("NFKC").toLowerCase().trim();
|
|
4432
|
+
if (!identity) return;
|
|
4433
|
+
return new Bun.CryptoHasher("sha256").update(identity).digest("hex");
|
|
4434
|
+
}
|
|
5014
4435
|
//#endregion
|
|
5015
4436
|
//#region src/review/publication-redaction.ts
|
|
5016
4437
|
function redactReviewPublication(options) {
|
|
@@ -5700,344 +5121,755 @@ async function runTaskRuntime(options) {
|
|
|
5700
5121
|
durationMs: Date.now() - started,
|
|
5701
5122
|
error: error instanceof Error ? error.message : String(error)
|
|
5702
5123
|
});
|
|
5703
|
-
if (options.log?.debugEnabled && error instanceof Error && error.stack) options.log.text("debug", "error stack", error.stack);
|
|
5704
|
-
return {
|
|
5705
|
-
taskName: task.name,
|
|
5706
|
-
output: {
|
|
5707
|
-
...output,
|
|
5708
|
-
check
|
|
5709
|
-
},
|
|
5710
|
-
error
|
|
5711
|
-
};
|
|
5712
|
-
}
|
|
5713
|
-
}));
|
|
5714
|
-
const taskChecks = taskResults.map((result) => runtimeTaskCheckResult(result.taskName, result.output.check ?? { conclusion: "success" }));
|
|
5715
|
-
const failedTask = taskResults.find((result) => result.error !== void 0);
|
|
5716
|
-
if (failedTask) {
|
|
5717
|
-
publishFailedRunTaskChecks(options, taskChecks);
|
|
5718
|
-
throw failedTask.error instanceof Error ? failedTask.error : new Error(String(failedTask.error));
|
|
5719
|
-
}
|
|
5720
|
-
const output = mergeTaskOutputs(taskResults);
|
|
5721
|
-
options.log?.info("task runtime collected", {
|
|
5722
|
-
findings: output.findings.length,
|
|
5723
|
-
providerModels: output.providerModels,
|
|
5724
|
-
repairAttempted: output.repairAttempted
|
|
5725
|
-
});
|
|
5726
|
-
const commandResponse = commandResponseResultFromOutput({
|
|
5727
|
-
provider,
|
|
5728
|
-
diffManifest,
|
|
5729
|
-
output,
|
|
5730
|
-
taskChecks,
|
|
5731
|
-
commandInvocation: options.commandInvocation,
|
|
5732
|
-
secretRedactor: options.secretRedactor
|
|
5733
|
-
});
|
|
5734
|
-
if (commandResponse) {
|
|
5735
|
-
publishTaskChecks(options.checkSink, commandResponse.taskChecks);
|
|
5736
|
-
return commandResponse;
|
|
5737
|
-
}
|
|
5738
|
-
assertReviewCommentOutput(output, options.commandInvocation !== void 0);
|
|
5739
|
-
const main = typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
|
|
5740
|
-
const validated = validateReviewResult(collectedReview(output, main), diffManifest, {
|
|
5741
|
-
expectedHeadSha: options.event.change.head.sha,
|
|
5742
|
-
pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
|
|
5743
|
-
});
|
|
5744
|
-
const verifier = await runSynchronizeVerifier({
|
|
5745
|
-
options,
|
|
5746
|
-
config,
|
|
5747
|
-
provider,
|
|
5748
|
-
diffManifest,
|
|
5749
|
-
priorReviewState,
|
|
5750
|
-
runId,
|
|
5751
|
-
piRunSink: runtimeOptions.piRunSink
|
|
5752
|
-
});
|
|
5753
|
-
const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
|
|
5754
|
-
const redactedPublication = redactReviewPublication({
|
|
5755
|
-
main,
|
|
5756
|
-
validated,
|
|
5757
|
-
threadActions: verifier.threadActions,
|
|
5758
|
-
taskChecks,
|
|
5124
|
+
if (options.log?.debugEnabled && error instanceof Error && error.stack) options.log.text("debug", "error stack", error.stack);
|
|
5125
|
+
return {
|
|
5126
|
+
taskName: task.name,
|
|
5127
|
+
output: {
|
|
5128
|
+
...output,
|
|
5129
|
+
check
|
|
5130
|
+
},
|
|
5131
|
+
error
|
|
5132
|
+
};
|
|
5133
|
+
}
|
|
5134
|
+
}));
|
|
5135
|
+
const taskChecks = taskResults.map((result) => runtimeTaskCheckResult(result.taskName, result.output.check ?? { conclusion: "success" }));
|
|
5136
|
+
const failedTask = taskResults.find((result) => result.error !== void 0);
|
|
5137
|
+
if (failedTask) {
|
|
5138
|
+
publishFailedRunTaskChecks(options, taskChecks);
|
|
5139
|
+
throw failedTask.error instanceof Error ? failedTask.error : new Error(String(failedTask.error));
|
|
5140
|
+
}
|
|
5141
|
+
const output = mergeTaskOutputs(taskResults);
|
|
5142
|
+
options.log?.info("task runtime collected", {
|
|
5143
|
+
findings: output.findings.length,
|
|
5144
|
+
providerModels: output.providerModels,
|
|
5145
|
+
repairAttempted: output.repairAttempted
|
|
5146
|
+
});
|
|
5147
|
+
const commandResponse = commandResponseResultFromOutput({
|
|
5148
|
+
provider,
|
|
5149
|
+
diffManifest,
|
|
5150
|
+
output,
|
|
5151
|
+
taskChecks,
|
|
5152
|
+
commandInvocation: options.commandInvocation,
|
|
5153
|
+
secretRedactor: options.secretRedactor
|
|
5154
|
+
});
|
|
5155
|
+
if (commandResponse) {
|
|
5156
|
+
publishTaskChecks(options.checkSink, commandResponse.taskChecks);
|
|
5157
|
+
return commandResponse;
|
|
5158
|
+
}
|
|
5159
|
+
assertReviewCommentOutput(output, options.commandInvocation !== void 0);
|
|
5160
|
+
const main = typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
|
|
5161
|
+
const validated = validateReviewResult(collectedReview(output, main), diffManifest, {
|
|
5162
|
+
expectedHeadSha: options.event.change.head.sha,
|
|
5163
|
+
pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
|
|
5164
|
+
});
|
|
5165
|
+
const verifier = await runSynchronizeVerifier({
|
|
5166
|
+
options,
|
|
5167
|
+
config,
|
|
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
|
|
5275
|
+
});
|
|
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,
|
|
5759
5370
|
redactor: options.secretRedactor
|
|
5760
5371
|
});
|
|
5761
|
-
|
|
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({
|
|
5762
5401
|
event: options.event,
|
|
5763
|
-
main:
|
|
5764
|
-
validated
|
|
5765
|
-
manifest: diffManifest,
|
|
5766
|
-
maxInlineComments: config.publication.maxInlineComments,
|
|
5767
|
-
maxStoredFindings: config.publication.maxStoredFindings,
|
|
5768
|
-
showHeader: config.publication.showHeader,
|
|
5769
|
-
showFooter: config.publication.showFooter,
|
|
5770
|
-
showStats: config.publication.showStats,
|
|
5771
|
-
priorReviewState: verifier.priorReviewState,
|
|
5772
|
-
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,
|
|
5773
5410
|
metadata: {
|
|
5774
5411
|
runtimeVersion,
|
|
5775
5412
|
configVersion: options.versionCompatibility?.configVersion,
|
|
5776
5413
|
trustedConfigSha: options.trustedConfigSha,
|
|
5777
5414
|
trustedConfigHash: options.trustedConfigHash,
|
|
5778
5415
|
reviewedHeadSha: options.event.change.head.sha,
|
|
5779
|
-
providerModels:
|
|
5780
|
-
selectedTasks,
|
|
5416
|
+
providerModels: [options.provider.model],
|
|
5417
|
+
selectedTasks: [],
|
|
5781
5418
|
failedTasks: [],
|
|
5782
|
-
validFindings:
|
|
5783
|
-
droppedFindings:
|
|
5784
|
-
...stats ? { stats } : {}
|
|
5419
|
+
validFindings: 0,
|
|
5420
|
+
droppedFindings: 0
|
|
5785
5421
|
}
|
|
5786
|
-
});
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
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);
|
|
5510
|
+
}
|
|
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
|
+
};
|
|
5795
5531
|
return {
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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
|
+
}
|
|
5806
5544
|
};
|
|
5807
5545
|
}
|
|
5808
|
-
function
|
|
5809
|
-
|
|
5810
|
-
body: "",
|
|
5811
|
-
taskChecks,
|
|
5812
|
-
redactor: options.secretRedactor
|
|
5813
|
-
});
|
|
5814
|
-
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);
|
|
5815
5548
|
}
|
|
5816
|
-
function
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
if (
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
commandResponse,
|
|
5823
|
-
commandInvocation: options.commandInvocation
|
|
5824
|
-
});
|
|
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;
|
|
5825
5555
|
}
|
|
5826
|
-
function
|
|
5827
|
-
|
|
5828
|
-
|
|
5556
|
+
function redact(value, secrets) {
|
|
5557
|
+
let redacted = value;
|
|
5558
|
+
for (const secret of secrets) redacted = redacted.split(secret).join("***");
|
|
5559
|
+
return redacted;
|
|
5829
5560
|
}
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
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 } : {}
|
|
5835
5582
|
};
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
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
|
|
5854
5685
|
});
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
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;
|
|
5704
|
+
},
|
|
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
|
+
};
|
|
5713
|
+
},
|
|
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);
|
|
5865
5737
|
},
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
base: options.event.change.base,
|
|
5873
|
-
head: options.event.change.head,
|
|
5874
|
-
isFork: options.event.change.isFork,
|
|
5875
|
-
async diffManifest(manifestOptions) {
|
|
5876
|
-
const key = JSON.stringify(manifestOptions ?? {});
|
|
5877
|
-
const cached = options.manifestCache.get(key);
|
|
5878
|
-
if (cached) return cloneDiffManifest(cached);
|
|
5879
|
-
const manifest = projectDiffManifest(options.diffManifest, manifestOptions);
|
|
5880
|
-
options.manifestCache.set(key, manifest);
|
|
5881
|
-
return cloneDiffManifest(manifest);
|
|
5882
|
-
},
|
|
5883
|
-
async changedFiles() {
|
|
5884
|
-
return options.diffManifest.files.map((file) => ({
|
|
5885
|
-
path: file.path,
|
|
5886
|
-
previousPath: file.previousPath,
|
|
5887
|
-
status: file.status
|
|
5888
|
-
}));
|
|
5889
|
-
},
|
|
5890
|
-
async currentHeadSha() {
|
|
5891
|
-
return options.event.change.head.sha;
|
|
5892
|
-
}
|
|
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
|
+
}));
|
|
5893
5744
|
},
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
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;
|
|
5901
5807
|
}
|
|
5902
|
-
} : void 0,
|
|
5903
|
-
secret(secret) {
|
|
5904
|
-
return resolveTaskSecret(secret, options);
|
|
5905
5808
|
},
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
agent,
|
|
5909
|
-
input,
|
|
5910
|
-
runOptions,
|
|
5911
|
-
runtime: {
|
|
5912
|
-
...options,
|
|
5913
|
-
taskContext,
|
|
5914
|
-
runId: options.runId,
|
|
5915
|
-
piRunSink: options.piRunSink
|
|
5916
|
-
}
|
|
5917
|
-
});
|
|
5918
|
-
options.output.providerModels.push(...result.providerModels);
|
|
5919
|
-
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
5920
|
-
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
5921
|
-
return agentOutputForTaskContext(agent, result.value);
|
|
5922
|
-
} },
|
|
5923
|
-
review: { async prior() {
|
|
5924
|
-
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
5925
|
-
} },
|
|
5926
|
-
check: createCheckHandle(options.output),
|
|
5927
|
-
async comment(value) {
|
|
5928
|
-
collectComment(options.output, value, options.taskName);
|
|
5809
|
+
async listThreads(repositoryId, changeNumber) {
|
|
5810
|
+
return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
|
|
5929
5811
|
},
|
|
5930
|
-
|
|
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
|
+
}
|
|
5931
5819
|
};
|
|
5932
|
-
return taskContext;
|
|
5933
5820
|
}
|
|
5934
|
-
function
|
|
5935
|
-
|
|
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
|
+
}
|
|
5936
5828
|
}
|
|
5937
|
-
function
|
|
5938
|
-
if (
|
|
5939
|
-
|
|
5940
|
-
if (
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
return
|
|
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";
|
|
5944
5836
|
}
|
|
5945
|
-
function
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
provider: options.provider,
|
|
5954
|
-
diffManifest: options.diffManifest,
|
|
5955
|
-
taskChecks: redacted.taskChecks,
|
|
5956
|
-
repairAttempted: options.output.repairAttempted,
|
|
5957
|
-
commandResponse: {
|
|
5958
|
-
commandName: options.commandInvocation.name,
|
|
5959
|
-
line: options.commandInvocation.line,
|
|
5960
|
-
arguments: options.commandInvocation.arguments,
|
|
5961
|
-
body: redacted.body
|
|
5962
|
-
}
|
|
5963
|
-
};
|
|
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);
|
|
5964
5845
|
}
|
|
5965
|
-
function
|
|
5966
|
-
|
|
5846
|
+
function collectionSchema(item) {
|
|
5847
|
+
return z.looseObject({
|
|
5848
|
+
count: z.number().int().nonnegative(),
|
|
5849
|
+
value: z.array(item)
|
|
5850
|
+
});
|
|
5967
5851
|
}
|
|
5968
|
-
function
|
|
5969
|
-
|
|
5970
|
-
const review = {
|
|
5971
|
-
summary: { body: reason },
|
|
5972
|
-
inlineFindings: []
|
|
5973
|
-
};
|
|
5974
|
-
const validated = {
|
|
5975
|
-
review,
|
|
5976
|
-
validFindings: [],
|
|
5977
|
-
droppedFindings: []
|
|
5978
|
-
};
|
|
5979
|
-
const publicationPlan = buildCommentPublishingPlan({
|
|
5980
|
-
event: options.event,
|
|
5981
|
-
main: reason,
|
|
5982
|
-
validated,
|
|
5983
|
-
manifest: options.diffManifest,
|
|
5984
|
-
maxInlineComments: options.config.publication.maxInlineComments,
|
|
5985
|
-
maxStoredFindings: options.config.publication.maxStoredFindings,
|
|
5986
|
-
showHeader: options.config.publication.showHeader,
|
|
5987
|
-
showFooter: options.config.publication.showFooter,
|
|
5988
|
-
showStats: options.config.publication.showStats,
|
|
5989
|
-
metadata: {
|
|
5990
|
-
runtimeVersion,
|
|
5991
|
-
configVersion: options.versionCompatibility?.configVersion,
|
|
5992
|
-
trustedConfigSha: options.trustedConfigSha,
|
|
5993
|
-
trustedConfigHash: options.trustedConfigHash,
|
|
5994
|
-
reviewedHeadSha: options.event.change.head.sha,
|
|
5995
|
-
providerModels: [options.provider.model],
|
|
5996
|
-
selectedTasks: [],
|
|
5997
|
-
failedTasks: [],
|
|
5998
|
-
validFindings: 0,
|
|
5999
|
-
droppedFindings: 0
|
|
6000
|
-
}
|
|
6001
|
-
}).publicationPlan;
|
|
6002
|
-
return {
|
|
6003
|
-
kind: "skipped",
|
|
6004
|
-
skipReason: reason,
|
|
6005
|
-
provider: options.provider,
|
|
6006
|
-
diffManifest: options.diffManifest,
|
|
6007
|
-
review,
|
|
6008
|
-
validated,
|
|
6009
|
-
publicationPlan,
|
|
6010
|
-
mainComment: publicationPlan.mainComment,
|
|
6011
|
-
inlineCommentDrafts: [],
|
|
6012
|
-
taskChecks: [],
|
|
6013
|
-
repairAttempted: false
|
|
6014
|
-
};
|
|
5852
|
+
function withApiVersion(path) {
|
|
5853
|
+
return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
|
|
6015
5854
|
}
|
|
6016
|
-
|
|
6017
|
-
//#region src/shared/secret-redactor-core.ts
|
|
6018
|
-
const redactedSecret = "[redacted secret]";
|
|
6019
|
-
const minimumSecretLength = 4;
|
|
6020
|
-
function createSecretRedactor(initialSecrets) {
|
|
6021
|
-
const secrets = new Set(initialSecrets.filter((value) => value.length >= minimumSecretLength));
|
|
5855
|
+
function jsonRequest$2(method, body) {
|
|
6022
5856
|
return {
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
redact(value) {
|
|
6027
|
-
const ordered = [...secrets].sort((left, right) => right.length - left.length);
|
|
6028
|
-
let next = value;
|
|
6029
|
-
for (const secret of ordered) next = next.replaceAll(secret, redactedSecret);
|
|
6030
|
-
return {
|
|
6031
|
-
value: next,
|
|
6032
|
-
detected: next !== value
|
|
6033
|
-
};
|
|
6034
|
-
}
|
|
5857
|
+
method,
|
|
5858
|
+
headers: { "Content-Type": "application/json" },
|
|
5859
|
+
body: JSON.stringify(body)
|
|
6035
5860
|
};
|
|
6036
5861
|
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
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(/^\/+/, "");
|
|
6041
5873
|
}
|
|
6042
5874
|
//#endregion
|
|
6043
5875
|
//#region src/hosts/env.ts
|
|
@@ -6417,8 +6249,17 @@ async function loadAzureDevOpsPriorReviewState(options) {
|
|
|
6417
6249
|
const state = extractPriorReviewState(await loadAzureDevOpsPriorMainComment(options), options.change.change.number);
|
|
6418
6250
|
if (!state) return void 0;
|
|
6419
6251
|
const owner = await authenticatedAzureOwner(options.client);
|
|
6420
|
-
const
|
|
6421
|
-
|
|
6252
|
+
const threads = await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number);
|
|
6253
|
+
const bodies = ownedThreadComments(threads, owner.uniqueName).map((comment) => comment.content);
|
|
6254
|
+
return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), threads.flatMap((thread) => {
|
|
6255
|
+
const root = thread.comments[0];
|
|
6256
|
+
const marker = root ? extractInlineFindingMarkerRecords([root.content])[0] : void 0;
|
|
6257
|
+
return root && marker && root.author?.uniqueName === owner.uniqueName ? [{
|
|
6258
|
+
findingId: marker.id,
|
|
6259
|
+
findingHeadSha: marker.head,
|
|
6260
|
+
resolved: isResolved(thread)
|
|
6261
|
+
}] : [];
|
|
6262
|
+
}));
|
|
6422
6263
|
}
|
|
6423
6264
|
async function loadAzureDevOpsPriorMainComment(options) {
|
|
6424
6265
|
const owner = await authenticatedAzureOwner(options.client);
|
|
@@ -6765,19 +6606,230 @@ function createAzureDevOpsHostAdapter(options = {}) {
|
|
|
6765
6606
|
};
|
|
6766
6607
|
}
|
|
6767
6608
|
}
|
|
6768
|
-
};
|
|
6769
|
-
}
|
|
6770
|
-
function azureCoordinates(change) {
|
|
6771
|
-
if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
|
|
6772
|
-
return change.coordinates;
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
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;
|
|
6798
|
+
}
|
|
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;
|
|
6773
6827
|
}
|
|
6774
|
-
function
|
|
6775
|
-
const repositoryId = slug.split("/").at(-1);
|
|
6776
|
-
if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
|
|
6828
|
+
function jsonRequest$1(method, body) {
|
|
6777
6829
|
return {
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6830
|
+
method,
|
|
6831
|
+
headers: { "Content-Type": "application/json" },
|
|
6832
|
+
body: JSON.stringify(body)
|
|
6781
6833
|
};
|
|
6782
6834
|
}
|
|
6783
6835
|
//#endregion
|
|
@@ -7027,7 +7079,14 @@ async function loadBitbucketPriorReviewState(options) {
|
|
|
7027
7079
|
const state = extractPriorReviewState(body ? normalizeBitbucketMarkdown(body) : void 0, options.change.change.number);
|
|
7028
7080
|
if (!state) return void 0;
|
|
7029
7081
|
const bodies = comments.map((comment) => normalizeBitbucketMarkdown(comment.content.raw));
|
|
7030
|
-
return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies)
|
|
7082
|
+
return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), comments.flatMap((comment) => {
|
|
7083
|
+
const marker = !comment.parent ? extractInlineFindingMarkerRecords([normalizeBitbucketMarkdown(comment.content.raw)])[0] : void 0;
|
|
7084
|
+
return marker ? [{
|
|
7085
|
+
findingId: marker.id,
|
|
7086
|
+
findingHeadSha: marker.head,
|
|
7087
|
+
resolved: comment.resolution !== void 0
|
|
7088
|
+
}] : [];
|
|
7089
|
+
}));
|
|
7031
7090
|
}
|
|
7032
7091
|
async function loadBitbucketPriorMainComment(options) {
|
|
7033
7092
|
const body = (await loadBitbucketOwnedComments(options)).find((comment) => normalizeBitbucketMarkdown(comment.content.raw).includes(mainMarker$1(options.change.change.number)))?.content.raw;
|
|
@@ -7893,23 +7952,20 @@ async function loadGitHubPriorReviewState(options) {
|
|
|
7893
7952
|
ownerLogin
|
|
7894
7953
|
}), options.change.change.number);
|
|
7895
7954
|
if (!state) return;
|
|
7896
|
-
const
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7955
|
+
const { ownerComments, threadByCommentId } = await loadOwnedReviewThreads(options, ownerLogin);
|
|
7956
|
+
const inlineBodies = ownerComments.map((comment) => comment.body ?? "");
|
|
7957
|
+
return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, inlineBodies), inlineBodies), ownerComments.flatMap((comment) => {
|
|
7958
|
+
const marker = extractInlineFindingMarkerRecords([comment.body ?? ""])[0];
|
|
7959
|
+
const thread = threadByCommentId.get(comment.id);
|
|
7960
|
+
return marker && thread ? [{
|
|
7961
|
+
findingId: marker.id,
|
|
7962
|
+
findingHeadSha: marker.head,
|
|
7963
|
+
resolved: thread.isResolved
|
|
7964
|
+
}] : [];
|
|
7965
|
+
}));
|
|
7901
7966
|
}
|
|
7902
7967
|
async function loadGitHubInlineThreadContexts(options) {
|
|
7903
|
-
const
|
|
7904
|
-
const comments = await options.client.listReviewComments({
|
|
7905
|
-
repo: options.change.repository.slug,
|
|
7906
|
-
pullRequestNumber: options.change.change.number
|
|
7907
|
-
});
|
|
7908
|
-
const ownerComments = comments.filter((comment) => comment.authorLogin === ownerLogin);
|
|
7909
|
-
const threadByCommentId = reviewThreadByCommentId(await options.client.listReviewThreads({
|
|
7910
|
-
repo: options.change.repository.slug,
|
|
7911
|
-
pullRequestNumber: options.change.change.number
|
|
7912
|
-
}));
|
|
7968
|
+
const { comments, ownerComments, threadByCommentId } = await loadOwnedReviewThreads(options, await options.client.getAuthenticatedUserLogin());
|
|
7913
7969
|
const commentById = new Map(comments.map((comment) => [comment.id, comment]));
|
|
7914
7970
|
return ownerComments.flatMap((comment) => {
|
|
7915
7971
|
const marker = extractInlineFindingMarkerRecords([comment.body ?? ""])[0];
|
|
@@ -7933,6 +7989,19 @@ async function loadGitHubInlineThreadContexts(options) {
|
|
|
7933
7989
|
}];
|
|
7934
7990
|
});
|
|
7935
7991
|
}
|
|
7992
|
+
async function loadOwnedReviewThreads(options, ownerLogin) {
|
|
7993
|
+
const coordinates = {
|
|
7994
|
+
repo: options.change.repository.slug,
|
|
7995
|
+
pullRequestNumber: options.change.change.number
|
|
7996
|
+
};
|
|
7997
|
+
const comments = await options.client.listReviewComments(coordinates);
|
|
7998
|
+
const threads = await options.client.listReviewThreads(coordinates);
|
|
7999
|
+
return {
|
|
8000
|
+
comments,
|
|
8001
|
+
ownerComments: comments.filter((comment) => comment.authorLogin === ownerLogin),
|
|
8002
|
+
threadByCommentId: reviewThreadByCommentId(threads)
|
|
8003
|
+
};
|
|
8004
|
+
}
|
|
7936
8005
|
async function loadGitHubPriorMainComment(options) {
|
|
7937
8006
|
const ownerLogin = options.ownerLogin ?? await options.client.getAuthenticatedUserLogin();
|
|
7938
8007
|
return findMainComment(await options.client.listIssueComments({
|
|
@@ -8477,6 +8546,213 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8477
8546
|
};
|
|
8478
8547
|
}
|
|
8479
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
|
|
8480
8756
|
//#region src/hosts/gitlab/event.ts
|
|
8481
8757
|
const projectSchema = z.looseObject({
|
|
8482
8758
|
id: z.union([z.number(), z.string()]),
|
|
@@ -8723,8 +8999,17 @@ async function loadGitLabPriorReviewState(options) {
|
|
|
8723
8999
|
const state = extractPriorReviewState(await loadGitLabPriorMainComment(options), options.change.change.number);
|
|
8724
9000
|
if (!state) return void 0;
|
|
8725
9001
|
const owner = await options.client.currentUser();
|
|
8726
|
-
const
|
|
8727
|
-
|
|
9002
|
+
const discussions = await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number);
|
|
9003
|
+
const bodies = discussionNotes(discussions).filter((note) => note.author?.username === owner.username).map((note) => note.body);
|
|
9004
|
+
return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), discussions.flatMap((discussion) => {
|
|
9005
|
+
const root = discussion.notes[0];
|
|
9006
|
+
const marker = root ? extractInlineFindingMarkerRecords([root.body])[0] : void 0;
|
|
9007
|
+
return root && marker && root.author?.username === owner.username ? [{
|
|
9008
|
+
findingId: marker.id,
|
|
9009
|
+
findingHeadSha: marker.head,
|
|
9010
|
+
resolved: root.resolved ?? false
|
|
9011
|
+
}] : [];
|
|
9012
|
+
}));
|
|
8728
9013
|
}
|
|
8729
9014
|
async function loadGitLabPriorMainComment(options) {
|
|
8730
9015
|
const owner = await options.client.currentUser();
|
|
@@ -9980,6 +10265,6 @@ async function runHostRunCommandWithDependencies(options) {
|
|
|
9980
10265
|
});
|
|
9981
10266
|
}
|
|
9982
10267
|
//#endregion
|
|
9983
|
-
export { piReadOnlyToolNames as _, runInspectCommand as a,
|
|
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 };
|
|
9984
10269
|
|
|
9985
|
-
//# sourceMappingURL=commands-
|
|
10270
|
+
//# sourceMappingURL=commands-B9qNW4pk.mjs.map
|