@usepipr/runtime 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/{commands-C5_GHGxN.d.mts → commands-YFjB3wx8.d.mts} +115 -214
- package/dist/commands-YFjB3wx8.d.mts.map +1 -0
- package/dist/{commands-RY37Y0rG.mjs → commands-ZZkXwqEn.mjs} +3251 -678
- package/dist/commands-ZZkXwqEn.mjs.map +1 -0
- package/dist/index.d.mts +18 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +410 -2
- package/dist/index.mjs.map +1 -0
- package/dist/internal/testing.d.mts +175 -2
- package/dist/internal/testing.d.mts.map +1 -0
- package/dist/internal/testing.mjs +2 -2
- package/package.json +3 -3
- package/dist/commands-C5_GHGxN.d.mts.map +0 -1
- package/dist/commands-RY37Y0rG.mjs.map +0 -1
|
@@ -295,6 +295,31 @@ const repositoryRefSchema = z.strictObject({
|
|
|
295
295
|
slug: nonEmptyStringSchema,
|
|
296
296
|
url: nonEmptyStringSchema.optional()
|
|
297
297
|
});
|
|
298
|
+
const codeHostCoordinatesSchema = z.discriminatedUnion("provider", [
|
|
299
|
+
z.strictObject({
|
|
300
|
+
provider: z.literal("github"),
|
|
301
|
+
owner: nonEmptyStringSchema,
|
|
302
|
+
repository: nonEmptyStringSchema
|
|
303
|
+
}),
|
|
304
|
+
z.strictObject({
|
|
305
|
+
provider: z.literal("gitlab"),
|
|
306
|
+
projectId: nonEmptyStringSchema,
|
|
307
|
+
projectPath: nonEmptyStringSchema
|
|
308
|
+
}),
|
|
309
|
+
z.strictObject({
|
|
310
|
+
provider: z.literal("bitbucket"),
|
|
311
|
+
workspace: nonEmptyStringSchema,
|
|
312
|
+
repository: nonEmptyStringSchema,
|
|
313
|
+
repositoryUuid: nonEmptyStringSchema.optional()
|
|
314
|
+
}),
|
|
315
|
+
z.strictObject({
|
|
316
|
+
provider: z.literal("azure-devops"),
|
|
317
|
+
organization: nonEmptyStringSchema,
|
|
318
|
+
project: nonEmptyStringSchema,
|
|
319
|
+
projectId: nonEmptyStringSchema.optional(),
|
|
320
|
+
repositoryId: nonEmptyStringSchema
|
|
321
|
+
})
|
|
322
|
+
]);
|
|
298
323
|
const changeEndpointSchema = z.strictObject({
|
|
299
324
|
sha: nonEmptyStringSchema,
|
|
300
325
|
ref: nonEmptyStringSchema.optional(),
|
|
@@ -310,7 +335,8 @@ const changeRequestRefSchema = z.strictObject({
|
|
|
310
335
|
author: z.strictObject({ login: nonEmptyStringSchema }).optional(),
|
|
311
336
|
base: changeEndpointSchema,
|
|
312
337
|
head: changeEndpointSchema,
|
|
313
|
-
isFork: z.boolean().optional()
|
|
338
|
+
isFork: z.boolean().optional(),
|
|
339
|
+
isDraft: z.boolean().optional()
|
|
314
340
|
});
|
|
315
341
|
const changeRequestEventContextSchema = z.strictObject({
|
|
316
342
|
eventName: nonEmptyStringSchema,
|
|
@@ -318,6 +344,7 @@ const changeRequestEventContextSchema = z.strictObject({
|
|
|
318
344
|
rawAction: nonEmptyStringSchema.optional(),
|
|
319
345
|
platform: platformInfoSchema,
|
|
320
346
|
repository: repositoryRefSchema,
|
|
347
|
+
coordinates: codeHostCoordinatesSchema.optional(),
|
|
321
348
|
change: changeRequestRefSchema,
|
|
322
349
|
workspace: nonEmptyStringSchema
|
|
323
350
|
});
|
|
@@ -529,7 +556,7 @@ function compareStableSemver(left, right) {
|
|
|
529
556
|
}
|
|
530
557
|
//#endregion
|
|
531
558
|
//#region src/shared/version.ts
|
|
532
|
-
const runtimeVersion = "0.
|
|
559
|
+
const runtimeVersion = "0.4.0";
|
|
533
560
|
//#endregion
|
|
534
561
|
//#region src/config/version-compat.ts
|
|
535
562
|
async function resolveConfigVersionCompatibility(options) {
|
|
@@ -3549,11 +3576,17 @@ function isOfficialInitRecipeId(recipe) {
|
|
|
3549
3576
|
}
|
|
3550
3577
|
//#endregion
|
|
3551
3578
|
//#region src/config/init.ts
|
|
3552
|
-
const supportedOfficialInitAdapters = [
|
|
3553
|
-
|
|
3554
|
-
|
|
3579
|
+
const supportedOfficialInitAdapters = [
|
|
3580
|
+
"github",
|
|
3581
|
+
"gitlab",
|
|
3582
|
+
"azure-devops",
|
|
3583
|
+
"bitbucket"
|
|
3584
|
+
];
|
|
3585
|
+
const defaultWorkflowActionRef = "somus/pipr@v0.4.0";
|
|
3586
|
+
const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.0";
|
|
3587
|
+
const defaultSdkVersion = "0.4.0";
|
|
3555
3588
|
function resolveOfficialInitAdapters(adapters) {
|
|
3556
|
-
if (adapters === void 0) return [
|
|
3589
|
+
if (adapters === void 0) return ["github"];
|
|
3557
3590
|
if (adapters.length === 0) return [];
|
|
3558
3591
|
const selected = /* @__PURE__ */ new Set();
|
|
3559
3592
|
for (const adapter of adapters) {
|
|
@@ -3562,7 +3595,7 @@ function resolveOfficialInitAdapters(adapters) {
|
|
|
3562
3595
|
if (adapters.length > 1) throw new Error("Adapter 'none' cannot be mixed with other init adapters.");
|
|
3563
3596
|
return [];
|
|
3564
3597
|
}
|
|
3565
|
-
if (adapter
|
|
3598
|
+
if (!supportedOfficialInitAdapters.includes(adapter)) throw unsupportedAdapterError(adapter);
|
|
3566
3599
|
selected.add(adapter);
|
|
3567
3600
|
}
|
|
3568
3601
|
return [...selected];
|
|
@@ -3633,8 +3666,66 @@ async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false
|
|
|
3633
3666
|
relativePath: path.join(".github", "workflows", "pipr.yml"),
|
|
3634
3667
|
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe, minimal)
|
|
3635
3668
|
});
|
|
3669
|
+
if (adapters.includes("gitlab")) files.push({
|
|
3670
|
+
relativePath: ".gitlab-ci.yml",
|
|
3671
|
+
contents: starterGitLabPipeline(relativeConfigDir.split(path.sep).join("/"), recipe)
|
|
3672
|
+
});
|
|
3673
|
+
if (adapters.includes("azure-devops")) files.push({
|
|
3674
|
+
relativePath: "azure-devops.pipr.env.example",
|
|
3675
|
+
contents: starterAzureDevOpsWebhookEnvironment(recipe)
|
|
3676
|
+
});
|
|
3677
|
+
if (adapters.includes("bitbucket")) files.push({
|
|
3678
|
+
relativePath: "bitbucket.pipr.env.example",
|
|
3679
|
+
contents: starterBitbucketWebhookEnvironment(recipe)
|
|
3680
|
+
});
|
|
3636
3681
|
return files;
|
|
3637
3682
|
}
|
|
3683
|
+
function starterGitLabPipeline(relativeConfigDir, recipe) {
|
|
3684
|
+
const lines = [
|
|
3685
|
+
"pipr:",
|
|
3686
|
+
" image:",
|
|
3687
|
+
` name: ${defaultGitLabImageRef}`,
|
|
3688
|
+
" entrypoint: [\"\"]",
|
|
3689
|
+
" rules:",
|
|
3690
|
+
" - if: '$CI_PIPELINE_SOURCE == \"merge_request_event\"'",
|
|
3691
|
+
" variables:",
|
|
3692
|
+
" GIT_DEPTH: \"0\"",
|
|
3693
|
+
" PIPR_CODE_HOST: \"gitlab\"",
|
|
3694
|
+
" script:",
|
|
3695
|
+
` - pipr host-run --host gitlab --config-dir ${relativeConfigDir}`
|
|
3696
|
+
];
|
|
3697
|
+
for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(` # Configure ${secret.env} as a masked GitLab CI/CD variable.`);
|
|
3698
|
+
lines.push("");
|
|
3699
|
+
return lines.join("\n");
|
|
3700
|
+
}
|
|
3701
|
+
function starterAzureDevOpsWebhookEnvironment(recipe) {
|
|
3702
|
+
const lines = [
|
|
3703
|
+
"# Copy these names into the trusted webhook runner's secret store.",
|
|
3704
|
+
"AZURE_DEVOPS_ORGANIZATION=",
|
|
3705
|
+
"AZURE_DEVOPS_PROJECT=",
|
|
3706
|
+
"AZURE_DEVOPS_BEARER_TOKEN=",
|
|
3707
|
+
"PIPR_AZURE_SUBSCRIPTION_ID=",
|
|
3708
|
+
"PIPR_WEBHOOK_SECRET="
|
|
3709
|
+
];
|
|
3710
|
+
for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(`${secret.env}=`);
|
|
3711
|
+
lines.push("");
|
|
3712
|
+
return lines.join("\n");
|
|
3713
|
+
}
|
|
3714
|
+
function starterBitbucketWebhookEnvironment(recipe) {
|
|
3715
|
+
const lines = [
|
|
3716
|
+
"# Copy these names into the trusted webhook runner's secret store.",
|
|
3717
|
+
"BITBUCKET_WORKSPACE=",
|
|
3718
|
+
"BITBUCKET_REPO_SLUG=",
|
|
3719
|
+
"BITBUCKET_EMAIL=",
|
|
3720
|
+
"BITBUCKET_API_TOKEN=",
|
|
3721
|
+
"BITBUCKET_PERMISSION_EMAIL=",
|
|
3722
|
+
"BITBUCKET_PERMISSION_API_TOKEN=",
|
|
3723
|
+
"PIPR_WEBHOOK_SECRET="
|
|
3724
|
+
];
|
|
3725
|
+
for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(`${secret.env}=`);
|
|
3726
|
+
lines.push("");
|
|
3727
|
+
return lines.join("\n");
|
|
3728
|
+
}
|
|
3638
3729
|
function starterPackageJson() {
|
|
3639
3730
|
return `${JSON.stringify({
|
|
3640
3731
|
private: true,
|
|
@@ -3753,6 +3844,797 @@ function starterWorkflow(relativeConfigDir, recipe, minimal = false) {
|
|
|
3753
3844
|
return lines.join("\n");
|
|
3754
3845
|
}
|
|
3755
3846
|
//#endregion
|
|
3847
|
+
//#region src/shared/redaction.ts
|
|
3848
|
+
const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
|
|
3849
|
+
function redactPotentialSecrets(value) {
|
|
3850
|
+
return value.replace(secretLikeTokenPattern, "[redacted secret]");
|
|
3851
|
+
}
|
|
3852
|
+
//#endregion
|
|
3853
|
+
//#region src/hosts/http.ts
|
|
3854
|
+
var CodeHostHttpError = class extends Error {
|
|
3855
|
+
status;
|
|
3856
|
+
constructor(message, status) {
|
|
3857
|
+
super(message);
|
|
3858
|
+
this.name = "CodeHostHttpError";
|
|
3859
|
+
this.status = status;
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
function createCodeHostHttpClient(options) {
|
|
3863
|
+
const fetchRequest = options.fetch ?? fetch;
|
|
3864
|
+
const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
|
|
3865
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
3866
|
+
const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
|
|
3867
|
+
const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
|
|
3868
|
+
let delayBeforeNextRequest = 0;
|
|
3869
|
+
return { async json(path, schema, init = {}) {
|
|
3870
|
+
if (delayBeforeNextRequest > 0) {
|
|
3871
|
+
await sleep(delayBeforeNextRequest);
|
|
3872
|
+
delayBeforeNextRequest = 0;
|
|
3873
|
+
}
|
|
3874
|
+
const method = init.method?.toUpperCase() ?? "GET";
|
|
3875
|
+
for (let attempt = 0;; attempt += 1) {
|
|
3876
|
+
const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
|
|
3877
|
+
const response = await fetchRequest(new URL(path, options.baseUrl), {
|
|
3878
|
+
...init,
|
|
3879
|
+
headers: {
|
|
3880
|
+
...Object.fromEntries(new Headers(options.headers)),
|
|
3881
|
+
...Object.fromEntries(new Headers(init.headers))
|
|
3882
|
+
},
|
|
3883
|
+
signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
3884
|
+
});
|
|
3885
|
+
const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
|
|
3886
|
+
if (response.ok) {
|
|
3887
|
+
delayBeforeNextRequest = retryAfter;
|
|
3888
|
+
return schema.parse(await response.json());
|
|
3889
|
+
}
|
|
3890
|
+
if (shouldRetryCodeHostRequest({
|
|
3891
|
+
method,
|
|
3892
|
+
attempt,
|
|
3893
|
+
maxRetries,
|
|
3894
|
+
response,
|
|
3895
|
+
retryStatuses: options.retryNonIdempotentStatuses
|
|
3896
|
+
})) {
|
|
3897
|
+
await sleep(retryAfter || 250 * 2 ** attempt);
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3900
|
+
const body = (await response.text()).slice(0, 1024);
|
|
3901
|
+
throw new CodeHostHttpError(redact$1(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
|
|
3902
|
+
}
|
|
3903
|
+
} };
|
|
3904
|
+
}
|
|
3905
|
+
function shouldRetryCodeHostRequest(options) {
|
|
3906
|
+
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);
|
|
3907
|
+
}
|
|
3908
|
+
function retryAfterMilliseconds(value) {
|
|
3909
|
+
if (!value) return 0;
|
|
3910
|
+
const seconds = Number(value);
|
|
3911
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
3912
|
+
const date = Date.parse(value);
|
|
3913
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
|
|
3914
|
+
}
|
|
3915
|
+
function redact$1(value, secrets) {
|
|
3916
|
+
let redacted = value;
|
|
3917
|
+
for (const secret of secrets) redacted = redacted.split(secret).join("***");
|
|
3918
|
+
return redactPotentialSecrets(redacted);
|
|
3919
|
+
}
|
|
3920
|
+
//#endregion
|
|
3921
|
+
//#region src/hosts/azure-devops/coordinates.ts
|
|
3922
|
+
function azureOrganizationFromUrl(value) {
|
|
3923
|
+
const url = new URL(value);
|
|
3924
|
+
return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
|
|
3925
|
+
}
|
|
3926
|
+
//#endregion
|
|
3927
|
+
//#region src/hosts/azure-devops/client.ts
|
|
3928
|
+
const identitySchema = z.looseObject({
|
|
3929
|
+
displayName: z.string().optional(),
|
|
3930
|
+
providerDisplayName: z.string().optional(),
|
|
3931
|
+
uniqueName: z.string().optional(),
|
|
3932
|
+
id: z.string().optional(),
|
|
3933
|
+
properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
|
|
3934
|
+
}).transform((identity) => {
|
|
3935
|
+
const displayName = identity.displayName ?? identity.providerDisplayName;
|
|
3936
|
+
const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
|
|
3937
|
+
return {
|
|
3938
|
+
...identity,
|
|
3939
|
+
...displayName ? { displayName } : {},
|
|
3940
|
+
...uniqueName ? { uniqueName } : {}
|
|
3941
|
+
};
|
|
3942
|
+
});
|
|
3943
|
+
const commitSchema = z.looseObject({ commitId: z.string().min(1) });
|
|
3944
|
+
const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
|
|
3945
|
+
const pullRequestSchema$1 = z.looseObject({
|
|
3946
|
+
pullRequestId: z.number().int().positive(),
|
|
3947
|
+
isDraft: z.boolean().optional(),
|
|
3948
|
+
title: z.string(),
|
|
3949
|
+
description: z.string().nullish(),
|
|
3950
|
+
url: z.string().optional(),
|
|
3951
|
+
sourceRefName: z.string().min(1),
|
|
3952
|
+
targetRefName: z.string().min(1),
|
|
3953
|
+
createdBy: identitySchema.optional(),
|
|
3954
|
+
lastMergeSourceCommit: commitSchema,
|
|
3955
|
+
lastMergeTargetCommit: commitSchema,
|
|
3956
|
+
forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
|
|
3957
|
+
repository: z.looseObject({
|
|
3958
|
+
id: z.string().min(1),
|
|
3959
|
+
name: z.string().min(1),
|
|
3960
|
+
url: z.string().optional(),
|
|
3961
|
+
project: z.looseObject({
|
|
3962
|
+
id: z.string().min(1),
|
|
3963
|
+
name: z.string().min(1)
|
|
3964
|
+
})
|
|
3965
|
+
})
|
|
3966
|
+
});
|
|
3967
|
+
const repositorySchema$1 = z.looseObject({
|
|
3968
|
+
id: z.string().min(1),
|
|
3969
|
+
name: z.string().min(1),
|
|
3970
|
+
project: z.looseObject({
|
|
3971
|
+
id: z.string().min(1),
|
|
3972
|
+
name: z.string().min(1)
|
|
3973
|
+
})
|
|
3974
|
+
});
|
|
3975
|
+
const iterationSchema = z.looseObject({
|
|
3976
|
+
id: z.number().int().positive(),
|
|
3977
|
+
sourceRefCommit: commitSchema
|
|
3978
|
+
});
|
|
3979
|
+
const iterationChangeSchema = z.looseObject({
|
|
3980
|
+
changeTrackingId: z.number().int(),
|
|
3981
|
+
changeType: z.string(),
|
|
3982
|
+
item: z.looseObject({
|
|
3983
|
+
path: z.string().min(1),
|
|
3984
|
+
originalPath: z.string().min(1).optional()
|
|
3985
|
+
})
|
|
3986
|
+
});
|
|
3987
|
+
const threadCommentSchema = z.looseObject({
|
|
3988
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
3989
|
+
parentCommentId: z.number().int().optional(),
|
|
3990
|
+
content: z.string().default(""),
|
|
3991
|
+
author: identitySchema.optional(),
|
|
3992
|
+
isDeleted: z.boolean().optional()
|
|
3993
|
+
});
|
|
3994
|
+
const pointSchema = z.looseObject({
|
|
3995
|
+
line: z.number().int(),
|
|
3996
|
+
offset: z.number().int()
|
|
3997
|
+
});
|
|
3998
|
+
const threadSchema = z.looseObject({
|
|
3999
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
4000
|
+
status: z.string().optional(),
|
|
4001
|
+
comments: z.array(threadCommentSchema).default([]),
|
|
4002
|
+
threadContext: z.looseObject({
|
|
4003
|
+
filePath: z.string().optional(),
|
|
4004
|
+
leftFileStart: pointSchema.nullish(),
|
|
4005
|
+
leftFileEnd: pointSchema.nullish(),
|
|
4006
|
+
rightFileStart: pointSchema.nullish(),
|
|
4007
|
+
rightFileEnd: pointSchema.nullish()
|
|
4008
|
+
}).nullish(),
|
|
4009
|
+
pullRequestThreadContext: z.looseObject({
|
|
4010
|
+
changeTrackingId: z.number().int().optional(),
|
|
4011
|
+
iterationContext: z.looseObject({
|
|
4012
|
+
firstComparingIteration: z.number().int(),
|
|
4013
|
+
secondComparingIteration: z.number().int()
|
|
4014
|
+
}).optional()
|
|
4015
|
+
}).nullish()
|
|
4016
|
+
});
|
|
4017
|
+
const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
|
|
4018
|
+
const aclSchema = collectionSchema(z.looseObject({
|
|
4019
|
+
inheritPermissions: z.boolean().default(true),
|
|
4020
|
+
acesDictionary: z.record(z.string(), z.looseObject({
|
|
4021
|
+
allow: z.number().int().default(0),
|
|
4022
|
+
deny: z.number().int().default(0),
|
|
4023
|
+
extendedInfo: z.looseObject({
|
|
4024
|
+
effectiveAllow: z.number().int().default(0),
|
|
4025
|
+
effectiveDeny: z.number().int().default(0)
|
|
4026
|
+
}).optional()
|
|
4027
|
+
}))
|
|
4028
|
+
}));
|
|
4029
|
+
function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch) {
|
|
4030
|
+
const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
|
|
4031
|
+
const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
|
|
4032
|
+
const pat = env.AZURE_DEVOPS_TOKEN;
|
|
4033
|
+
const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
|
|
4034
|
+
if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
|
|
4035
|
+
if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
|
|
4036
|
+
if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
|
|
4037
|
+
const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
|
|
4038
|
+
const api = createCodeHostHttpClient({
|
|
4039
|
+
baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
|
|
4040
|
+
headers,
|
|
4041
|
+
fetch
|
|
4042
|
+
});
|
|
4043
|
+
const organizationApi = createCodeHostHttpClient({
|
|
4044
|
+
baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
|
|
4045
|
+
headers,
|
|
4046
|
+
fetch
|
|
4047
|
+
});
|
|
4048
|
+
const identityApi = createCodeHostHttpClient({
|
|
4049
|
+
baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
|
|
4050
|
+
headers,
|
|
4051
|
+
fetch
|
|
4052
|
+
});
|
|
4053
|
+
const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
|
|
4054
|
+
return {
|
|
4055
|
+
organization,
|
|
4056
|
+
project,
|
|
4057
|
+
async currentUser() {
|
|
4058
|
+
return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
|
|
4059
|
+
},
|
|
4060
|
+
async getRepository(repository) {
|
|
4061
|
+
const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
|
|
4062
|
+
return {
|
|
4063
|
+
id: value.id,
|
|
4064
|
+
name: value.name,
|
|
4065
|
+
projectId: value.project.id,
|
|
4066
|
+
project: value.project.name
|
|
4067
|
+
};
|
|
4068
|
+
},
|
|
4069
|
+
async getRepositoryPermission(actor, projectId, repositoryId) {
|
|
4070
|
+
const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
|
|
4071
|
+
descriptor: z.string().min(1),
|
|
4072
|
+
isActive: z.boolean().optional(),
|
|
4073
|
+
isContainer: z.boolean().optional(),
|
|
4074
|
+
memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
|
|
4075
|
+
})))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
|
|
4076
|
+
if (candidates.length !== 1) return "none";
|
|
4077
|
+
const identity = candidates[0];
|
|
4078
|
+
if (!identity) return "none";
|
|
4079
|
+
const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
|
|
4080
|
+
const loadAcls = (token) => {
|
|
4081
|
+
const query = new URLSearchParams({
|
|
4082
|
+
token,
|
|
4083
|
+
descriptors: descriptors.join(","),
|
|
4084
|
+
includeExtendedInfo: "true",
|
|
4085
|
+
recurse: "false",
|
|
4086
|
+
"api-version": "7.1"
|
|
4087
|
+
});
|
|
4088
|
+
return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
|
|
4089
|
+
};
|
|
4090
|
+
const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
|
|
4091
|
+
return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
|
|
4092
|
+
},
|
|
4093
|
+
getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
|
|
4094
|
+
async listIterations(repositoryId, changeNumber) {
|
|
4095
|
+
return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
|
|
4096
|
+
id: iteration.id,
|
|
4097
|
+
headSha: iteration.sourceRefCommit.commitId
|
|
4098
|
+
}));
|
|
4099
|
+
},
|
|
4100
|
+
async loadChange(options) {
|
|
4101
|
+
if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
|
|
4102
|
+
const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
|
|
4103
|
+
const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
|
|
4104
|
+
const headSha = pullRequest.lastMergeSourceCommit.commitId;
|
|
4105
|
+
const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
|
|
4106
|
+
if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
|
|
4107
|
+
const sourceRef = branchName(pullRequest.sourceRefName);
|
|
4108
|
+
const targetRef = branchName(pullRequest.targetRefName);
|
|
4109
|
+
return {
|
|
4110
|
+
repository: {
|
|
4111
|
+
slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
|
|
4112
|
+
url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
|
|
4113
|
+
},
|
|
4114
|
+
coordinates: {
|
|
4115
|
+
provider: "azure-devops",
|
|
4116
|
+
organization,
|
|
4117
|
+
project: pullRequest.repository.project.name,
|
|
4118
|
+
projectId: pullRequest.repository.project.id,
|
|
4119
|
+
repositoryId: pullRequest.repository.id
|
|
4120
|
+
},
|
|
4121
|
+
change: {
|
|
4122
|
+
number: pullRequest.pullRequestId,
|
|
4123
|
+
isDraft: pullRequest.isDraft,
|
|
4124
|
+
title: pullRequest.title,
|
|
4125
|
+
description: pullRequest.description ?? "",
|
|
4126
|
+
url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
|
|
4127
|
+
author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
|
|
4128
|
+
base: {
|
|
4129
|
+
sha: pullRequest.lastMergeTargetCommit.commitId,
|
|
4130
|
+
ref: targetRef
|
|
4131
|
+
},
|
|
4132
|
+
head: {
|
|
4133
|
+
sha: headSha,
|
|
4134
|
+
ref: sourceRef,
|
|
4135
|
+
...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
|
|
4136
|
+
},
|
|
4137
|
+
...pullRequest.forkSource ? { isFork: true } : {}
|
|
4138
|
+
},
|
|
4139
|
+
iterationId: iteration.id
|
|
4140
|
+
};
|
|
4141
|
+
},
|
|
4142
|
+
async listIterationChanges(repositoryId, changeNumber, iterationId) {
|
|
4143
|
+
const changes = [];
|
|
4144
|
+
let skip = 0;
|
|
4145
|
+
let top = 2e3;
|
|
4146
|
+
for (;;) {
|
|
4147
|
+
const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
|
|
4148
|
+
const page = await api.json(withApiVersion(path), z.looseObject({
|
|
4149
|
+
changeEntries: z.array(iterationChangeSchema),
|
|
4150
|
+
nextSkip: z.number().int().nonnegative().optional(),
|
|
4151
|
+
nextTop: z.number().int().nonnegative().max(2e3).optional()
|
|
4152
|
+
}));
|
|
4153
|
+
changes.push(...page.changeEntries.map((change) => ({
|
|
4154
|
+
changeTrackingId: change.changeTrackingId,
|
|
4155
|
+
changeType: change.changeType,
|
|
4156
|
+
path: trimLeadingSlash(change.item.path),
|
|
4157
|
+
...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
|
|
4158
|
+
})));
|
|
4159
|
+
if (!page.nextSkip) return changes;
|
|
4160
|
+
skip = page.nextSkip;
|
|
4161
|
+
if (page.nextTop) top = page.nextTop;
|
|
4162
|
+
}
|
|
4163
|
+
},
|
|
4164
|
+
async listThreads(repositoryId, changeNumber) {
|
|
4165
|
+
return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
|
|
4166
|
+
},
|
|
4167
|
+
createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
|
|
4168
|
+
updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
|
|
4169
|
+
createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
|
|
4170
|
+
updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
|
|
4171
|
+
async createStatus(repositoryId, changeNumber, body) {
|
|
4172
|
+
return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
|
|
4173
|
+
}
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
function azureDevOpsStatusState(state) {
|
|
4177
|
+
switch (state) {
|
|
4178
|
+
case "pending": return "pending";
|
|
4179
|
+
case "success": return "succeeded";
|
|
4180
|
+
case "failure": return "failed";
|
|
4181
|
+
case "neutral": return "notApplicable";
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
function azureRepositoryPermission(bits) {
|
|
4185
|
+
if ((bits & 2) === 0) return "none";
|
|
4186
|
+
if ((bits & 8192) !== 0) return "admin";
|
|
4187
|
+
if ((bits & 2048) !== 0) return "maintain";
|
|
4188
|
+
if ((bits & 4) !== 0) return "write";
|
|
4189
|
+
if ((bits & 16384) !== 0) return "triage";
|
|
4190
|
+
return "read";
|
|
4191
|
+
}
|
|
4192
|
+
function azurePermissionFromAcls(acls) {
|
|
4193
|
+
let allow = 0;
|
|
4194
|
+
let deny = 0;
|
|
4195
|
+
for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
|
|
4196
|
+
allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
|
|
4197
|
+
deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
|
|
4198
|
+
}
|
|
4199
|
+
return azureRepositoryPermission(allow & ~deny);
|
|
4200
|
+
}
|
|
4201
|
+
function collectionSchema(item) {
|
|
4202
|
+
return z.looseObject({
|
|
4203
|
+
count: z.number().int().nonnegative(),
|
|
4204
|
+
value: z.array(item)
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
function withApiVersion(path) {
|
|
4208
|
+
return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
|
|
4209
|
+
}
|
|
4210
|
+
function jsonRequest$2(method, body) {
|
|
4211
|
+
return {
|
|
4212
|
+
method,
|
|
4213
|
+
headers: { "Content-Type": "application/json" },
|
|
4214
|
+
body: JSON.stringify(body)
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
function organizationFromCollectionUri$1(value) {
|
|
4218
|
+
return value ? azureOrganizationFromUrl(value) : void 0;
|
|
4219
|
+
}
|
|
4220
|
+
function branchName(ref) {
|
|
4221
|
+
return ref.replace(/^refs\/heads\//, "");
|
|
4222
|
+
}
|
|
4223
|
+
function repositoryWebUrl(organization, project, repository) {
|
|
4224
|
+
return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
|
|
4225
|
+
}
|
|
4226
|
+
function trimLeadingSlash(value) {
|
|
4227
|
+
return value.replace(/^\/+/, "");
|
|
4228
|
+
}
|
|
4229
|
+
//#endregion
|
|
4230
|
+
//#region src/hosts/bitbucket/schema.ts
|
|
4231
|
+
const bitbucketRepositorySchema = z.looseObject({
|
|
4232
|
+
uuid: z.string().min(1),
|
|
4233
|
+
name: z.string().min(1),
|
|
4234
|
+
full_name: z.string().min(1),
|
|
4235
|
+
slug: z.string().min(1).optional(),
|
|
4236
|
+
links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
|
|
4237
|
+
}).transform((repository) => ({
|
|
4238
|
+
...repository,
|
|
4239
|
+
slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
|
|
4240
|
+
}));
|
|
4241
|
+
//#endregion
|
|
4242
|
+
//#region src/hosts/bitbucket/client.ts
|
|
4243
|
+
const userSchema$1 = z.looseObject({
|
|
4244
|
+
uuid: z.string().optional(),
|
|
4245
|
+
nickname: z.string().optional(),
|
|
4246
|
+
display_name: z.string().optional()
|
|
4247
|
+
});
|
|
4248
|
+
const endpointSchema = z.looseObject({
|
|
4249
|
+
branch: z.looseObject({ name: z.string().min(1) }),
|
|
4250
|
+
commit: z.looseObject({ hash: z.string().min(1) }),
|
|
4251
|
+
repository: bitbucketRepositorySchema
|
|
4252
|
+
});
|
|
4253
|
+
const pullRequestSchema = z.looseObject({
|
|
4254
|
+
id: z.number().int().positive(),
|
|
4255
|
+
draft: z.boolean().optional(),
|
|
4256
|
+
title: z.string(),
|
|
4257
|
+
description: z.string().default(""),
|
|
4258
|
+
author: userSchema$1.optional(),
|
|
4259
|
+
source: endpointSchema,
|
|
4260
|
+
destination: endpointSchema,
|
|
4261
|
+
links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
|
|
4262
|
+
});
|
|
4263
|
+
const inlineSchema = z.looseObject({
|
|
4264
|
+
path: z.string().optional(),
|
|
4265
|
+
from: z.number().int().nullable().optional(),
|
|
4266
|
+
to: z.number().int().nullable().optional(),
|
|
4267
|
+
start_from: z.number().int().nullable().optional(),
|
|
4268
|
+
start_to: z.number().int().nullable().optional()
|
|
4269
|
+
});
|
|
4270
|
+
const commentSchema = z.looseObject({
|
|
4271
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
4272
|
+
content: z.looseObject({ raw: z.string().default("") }),
|
|
4273
|
+
user: userSchema$1.optional(),
|
|
4274
|
+
parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
|
|
4275
|
+
inline: inlineSchema.optional(),
|
|
4276
|
+
deleted: z.boolean().optional(),
|
|
4277
|
+
resolution: z.looseObject({}).optional()
|
|
4278
|
+
});
|
|
4279
|
+
function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
|
|
4280
|
+
const workspace = env.BITBUCKET_WORKSPACE;
|
|
4281
|
+
const repository = env.BITBUCKET_REPO_SLUG;
|
|
4282
|
+
const token = env.BITBUCKET_API_TOKEN;
|
|
4283
|
+
const email = env.BITBUCKET_EMAIL;
|
|
4284
|
+
if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
|
|
4285
|
+
if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
|
|
4286
|
+
if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
|
|
4287
|
+
const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
|
|
4288
|
+
const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
|
|
4289
|
+
const api = createCodeHostHttpClient({
|
|
4290
|
+
baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
|
|
4291
|
+
headers: { Authorization: authorization },
|
|
4292
|
+
fetch
|
|
4293
|
+
});
|
|
4294
|
+
const rootApi = createCodeHostHttpClient({
|
|
4295
|
+
baseUrl: "https://api.bitbucket.org/2.0/",
|
|
4296
|
+
headers: { Authorization: authorization },
|
|
4297
|
+
fetch
|
|
4298
|
+
});
|
|
4299
|
+
const prPath = (id) => `pullrequests/${id}`;
|
|
4300
|
+
return {
|
|
4301
|
+
workspace,
|
|
4302
|
+
repository,
|
|
4303
|
+
async currentUser() {
|
|
4304
|
+
const value = await rootApi.json("user", userSchema$1);
|
|
4305
|
+
return {
|
|
4306
|
+
uuid: value.uuid,
|
|
4307
|
+
nickname: value.nickname,
|
|
4308
|
+
displayName: value.display_name
|
|
4309
|
+
};
|
|
4310
|
+
},
|
|
4311
|
+
async getRepository() {
|
|
4312
|
+
const value = await api.json("", bitbucketRepositorySchema);
|
|
4313
|
+
return {
|
|
4314
|
+
uuid: value.uuid,
|
|
4315
|
+
slug: value.slug,
|
|
4316
|
+
fullName: value.full_name,
|
|
4317
|
+
url: value.links.html.href
|
|
4318
|
+
};
|
|
4319
|
+
},
|
|
4320
|
+
async getRepositoryPermission(actor, repositoryUuid) {
|
|
4321
|
+
const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
|
|
4322
|
+
const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
|
|
4323
|
+
if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
|
|
4324
|
+
const permissionApi = createCodeHostHttpClient({
|
|
4325
|
+
baseUrl: "https://api.bitbucket.org/2.0/",
|
|
4326
|
+
headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
|
|
4327
|
+
fetch
|
|
4328
|
+
});
|
|
4329
|
+
const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
|
|
4330
|
+
return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
|
|
4331
|
+
permission: z.enum([
|
|
4332
|
+
"read",
|
|
4333
|
+
"write",
|
|
4334
|
+
"admin"
|
|
4335
|
+
]),
|
|
4336
|
+
user: userSchema$1
|
|
4337
|
+
})))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
|
|
4338
|
+
},
|
|
4339
|
+
getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
|
|
4340
|
+
async loadChange(options) {
|
|
4341
|
+
if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
|
|
4342
|
+
const pullRequest = await this.getPullRequest(options.changeNumber);
|
|
4343
|
+
return {
|
|
4344
|
+
repository: {
|
|
4345
|
+
slug: pullRequest.destination.repository.full_name,
|
|
4346
|
+
url: pullRequest.destination.repository.links.html.href
|
|
4347
|
+
},
|
|
4348
|
+
coordinates: {
|
|
4349
|
+
provider: "bitbucket",
|
|
4350
|
+
workspace,
|
|
4351
|
+
repository,
|
|
4352
|
+
repositoryUuid: pullRequest.destination.repository.uuid
|
|
4353
|
+
},
|
|
4354
|
+
change: {
|
|
4355
|
+
number: pullRequest.id,
|
|
4356
|
+
isDraft: pullRequest.draft,
|
|
4357
|
+
title: pullRequest.title,
|
|
4358
|
+
description: pullRequest.description,
|
|
4359
|
+
url: pullRequest.links.html.href,
|
|
4360
|
+
author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
|
|
4361
|
+
base: {
|
|
4362
|
+
sha: pullRequest.destination.commit.hash,
|
|
4363
|
+
ref: pullRequest.destination.branch.name,
|
|
4364
|
+
url: pullRequest.destination.repository.links.html.href
|
|
4365
|
+
},
|
|
4366
|
+
head: {
|
|
4367
|
+
sha: pullRequest.source.commit.hash,
|
|
4368
|
+
ref: pullRequest.source.branch.name,
|
|
4369
|
+
url: pullRequest.source.repository.links.html.href
|
|
4370
|
+
},
|
|
4371
|
+
isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
},
|
|
4375
|
+
async listComments(id) {
|
|
4376
|
+
return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
|
|
4377
|
+
},
|
|
4378
|
+
createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
|
|
4379
|
+
updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
|
|
4380
|
+
async replyToComment(id, commentId, content) {
|
|
4381
|
+
const parentId = positiveCommentId(commentId);
|
|
4382
|
+
return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
|
|
4383
|
+
content: { raw: content },
|
|
4384
|
+
parent: { id: parentId }
|
|
4385
|
+
}));
|
|
4386
|
+
},
|
|
4387
|
+
async resolveComment(id, commentId) {
|
|
4388
|
+
positiveCommentId(commentId);
|
|
4389
|
+
await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
|
|
4390
|
+
},
|
|
4391
|
+
async setStatus(sha, key, body) {
|
|
4392
|
+
return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
|
|
4393
|
+
...body,
|
|
4394
|
+
key
|
|
4395
|
+
}))).key;
|
|
4396
|
+
}
|
|
4397
|
+
};
|
|
4398
|
+
}
|
|
4399
|
+
function positiveCommentId(value) {
|
|
4400
|
+
const id = Number(value);
|
|
4401
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
|
|
4402
|
+
return id;
|
|
4403
|
+
}
|
|
4404
|
+
function bitbucketStatusState(state) {
|
|
4405
|
+
if (state === "pending") return "INPROGRESS";
|
|
4406
|
+
if (state === "failure") return "FAILED";
|
|
4407
|
+
if (state === "neutral") return "STOPPED";
|
|
4408
|
+
return "SUCCESSFUL";
|
|
4409
|
+
}
|
|
4410
|
+
function escapeBitbucketQueryValue(value) {
|
|
4411
|
+
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
4412
|
+
}
|
|
4413
|
+
function pagedSchema(item) {
|
|
4414
|
+
return z.looseObject({
|
|
4415
|
+
values: z.array(item),
|
|
4416
|
+
next: z.string().url().optional()
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
async function listAll(api, path, schema, allowedPathPrefix) {
|
|
4420
|
+
const values = [];
|
|
4421
|
+
let next = path;
|
|
4422
|
+
while (next) {
|
|
4423
|
+
const page = await api.json(next, pagedSchema(schema));
|
|
4424
|
+
values.push(...page.values);
|
|
4425
|
+
if (page.next) {
|
|
4426
|
+
const url = new URL(page.next);
|
|
4427
|
+
if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
|
|
4428
|
+
}
|
|
4429
|
+
next = page.next;
|
|
4430
|
+
}
|
|
4431
|
+
return values;
|
|
4432
|
+
}
|
|
4433
|
+
function jsonRequest$1(method, body) {
|
|
4434
|
+
return {
|
|
4435
|
+
method,
|
|
4436
|
+
headers: { "Content-Type": "application/json" },
|
|
4437
|
+
body: JSON.stringify(body)
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
//#endregion
|
|
4441
|
+
//#region src/hosts/gitlab/client.ts
|
|
4442
|
+
const userSchema = z.looseObject({
|
|
4443
|
+
id: z.number().int().positive(),
|
|
4444
|
+
username: z.string().min(1)
|
|
4445
|
+
});
|
|
4446
|
+
const noteSchema = z.looseObject({
|
|
4447
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
4448
|
+
body: z.string(),
|
|
4449
|
+
author: userSchema.optional()
|
|
4450
|
+
});
|
|
4451
|
+
const positionSchema = z.looseObject({
|
|
4452
|
+
new_path: z.string().optional(),
|
|
4453
|
+
old_path: z.string().optional(),
|
|
4454
|
+
new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
|
|
4455
|
+
old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0)
|
|
4456
|
+
});
|
|
4457
|
+
const discussionNoteSchema = noteSchema.extend({
|
|
4458
|
+
resolvable: z.boolean().optional(),
|
|
4459
|
+
resolved: z.boolean().optional(),
|
|
4460
|
+
position: positionSchema.nullable().optional()
|
|
4461
|
+
});
|
|
4462
|
+
const discussionSchema = z.looseObject({
|
|
4463
|
+
id: z.string().min(1),
|
|
4464
|
+
individual_note: z.boolean().optional(),
|
|
4465
|
+
notes: z.array(discussionNoteSchema)
|
|
4466
|
+
});
|
|
4467
|
+
const diffRefsSchema = z.looseObject({
|
|
4468
|
+
base_sha: z.string().min(1),
|
|
4469
|
+
start_sha: z.string().min(1),
|
|
4470
|
+
head_sha: z.string().min(1)
|
|
4471
|
+
});
|
|
4472
|
+
const mergeRequestResponseSchema = z.looseObject({
|
|
4473
|
+
iid: z.number().int().positive(),
|
|
4474
|
+
title: z.string().default(""),
|
|
4475
|
+
description: z.string().nullable().optional(),
|
|
4476
|
+
web_url: z.string().url().optional(),
|
|
4477
|
+
source_branch: z.string().min(1),
|
|
4478
|
+
target_branch: z.string().min(1),
|
|
4479
|
+
source_project_id: z.number().int().positive(),
|
|
4480
|
+
target_project_id: z.number().int().positive(),
|
|
4481
|
+
sha: z.string().min(1),
|
|
4482
|
+
draft: z.boolean().optional(),
|
|
4483
|
+
author: userSchema.optional(),
|
|
4484
|
+
references: z.looseObject({ full: z.string().optional() }).optional(),
|
|
4485
|
+
diff_refs: diffRefsSchema.nullable().optional()
|
|
4486
|
+
});
|
|
4487
|
+
const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
|
|
4488
|
+
const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
|
|
4489
|
+
const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
|
|
4490
|
+
function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
|
|
4491
|
+
const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
|
|
4492
|
+
if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
|
|
4493
|
+
const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
|
|
4494
|
+
const clientOptions = {
|
|
4495
|
+
baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
|
|
4496
|
+
headers,
|
|
4497
|
+
fetch,
|
|
4498
|
+
sleep: async (milliseconds) => void await sleep(milliseconds)
|
|
4499
|
+
};
|
|
4500
|
+
const api = createCodeHostHttpClient(clientOptions);
|
|
4501
|
+
const statusApi = createCodeHostHttpClient({
|
|
4502
|
+
...clientOptions,
|
|
4503
|
+
retryNonIdempotentStatuses: [409]
|
|
4504
|
+
});
|
|
4505
|
+
return {
|
|
4506
|
+
async getProject(project) {
|
|
4507
|
+
const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
|
|
4508
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
4509
|
+
path_with_namespace: z.string().min(1)
|
|
4510
|
+
}));
|
|
4511
|
+
return {
|
|
4512
|
+
id: value.id,
|
|
4513
|
+
path: value.path_with_namespace
|
|
4514
|
+
};
|
|
4515
|
+
},
|
|
4516
|
+
currentUser: () => api.json("user", userSchema),
|
|
4517
|
+
async loadChange(options) {
|
|
4518
|
+
const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
|
|
4519
|
+
return {
|
|
4520
|
+
repository: {
|
|
4521
|
+
slug: options.projectPath,
|
|
4522
|
+
url: projectWebUrl(mergeRequest.web_url)
|
|
4523
|
+
},
|
|
4524
|
+
coordinates: {
|
|
4525
|
+
provider: "gitlab",
|
|
4526
|
+
projectId: String(mergeRequest.target_project_id),
|
|
4527
|
+
projectPath: options.projectPath
|
|
4528
|
+
},
|
|
4529
|
+
change: {
|
|
4530
|
+
number: mergeRequest.iid,
|
|
4531
|
+
title: mergeRequest.title,
|
|
4532
|
+
description: mergeRequest.description ?? "",
|
|
4533
|
+
url: mergeRequest.web_url,
|
|
4534
|
+
author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
|
|
4535
|
+
base: {
|
|
4536
|
+
sha: mergeRequest.diff_refs.start_sha,
|
|
4537
|
+
ref: mergeRequest.target_branch
|
|
4538
|
+
},
|
|
4539
|
+
head: {
|
|
4540
|
+
sha: mergeRequest.diff_refs.head_sha,
|
|
4541
|
+
ref: mergeRequest.source_branch
|
|
4542
|
+
},
|
|
4543
|
+
isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
|
|
4544
|
+
isDraft: mergeRequest.draft
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
},
|
|
4548
|
+
async getMergeRequest(projectId, changeNumber) {
|
|
4549
|
+
const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
|
|
4550
|
+
for (let attempt = 0;; attempt += 1) {
|
|
4551
|
+
const response = await api.json(requestPath, mergeRequestResponseSchema);
|
|
4552
|
+
const prepared = mergeRequestSchema.safeParse(response);
|
|
4553
|
+
if (prepared.success) return prepared.data;
|
|
4554
|
+
if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
|
|
4555
|
+
await sleep(250 * 2 ** attempt);
|
|
4556
|
+
}
|
|
4557
|
+
},
|
|
4558
|
+
async getRepositoryPermission(projectId, username) {
|
|
4559
|
+
const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
|
|
4560
|
+
if (!user) return "none";
|
|
4561
|
+
try {
|
|
4562
|
+
return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
if (error instanceof CodeHostHttpError && error.status === 404) return "none";
|
|
4565
|
+
throw error;
|
|
4566
|
+
}
|
|
4567
|
+
},
|
|
4568
|
+
listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
|
|
4569
|
+
createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
|
|
4570
|
+
updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
|
|
4571
|
+
listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
|
|
4572
|
+
getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
|
|
4573
|
+
async findReplyParent(projectId, changeNumber, noteId, discussionId) {
|
|
4574
|
+
if (!discussionId) return void 0;
|
|
4575
|
+
const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
|
|
4576
|
+
if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
|
|
4577
|
+
return discussion.notes[0]?.id;
|
|
4578
|
+
},
|
|
4579
|
+
createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
|
|
4580
|
+
body,
|
|
4581
|
+
position
|
|
4582
|
+
})),
|
|
4583
|
+
replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
|
|
4584
|
+
async resolveDiscussion(projectId, changeNumber, discussionId) {
|
|
4585
|
+
await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
|
|
4586
|
+
},
|
|
4587
|
+
async setStatus(projectId, sha, name, state, description) {
|
|
4588
|
+
const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
|
|
4589
|
+
state: gitLabStatusState(state),
|
|
4590
|
+
name,
|
|
4591
|
+
description: boundedStatusDescription(description)
|
|
4592
|
+
}));
|
|
4593
|
+
return String(status.id ?? name);
|
|
4594
|
+
}
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
const MAX_PAGINATION_PAGES = 100;
|
|
4598
|
+
async function paginated(client, path, schema) {
|
|
4599
|
+
const values = [];
|
|
4600
|
+
for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
|
|
4601
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
4602
|
+
const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
|
|
4603
|
+
values.push(...batch);
|
|
4604
|
+
if (batch.length < 100) return values;
|
|
4605
|
+
}
|
|
4606
|
+
throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
|
|
4607
|
+
}
|
|
4608
|
+
function jsonRequest(method, body) {
|
|
4609
|
+
return {
|
|
4610
|
+
method,
|
|
4611
|
+
headers: { "Content-Type": "application/json" },
|
|
4612
|
+
body: JSON.stringify(body)
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
function accessLevelPermission(level) {
|
|
4616
|
+
if (level >= 50) return "admin";
|
|
4617
|
+
if (level >= 40) return "maintain";
|
|
4618
|
+
if (level >= 30) return "write";
|
|
4619
|
+
if (level >= 15) return "triage";
|
|
4620
|
+
if (level >= 10) return "read";
|
|
4621
|
+
return "none";
|
|
4622
|
+
}
|
|
4623
|
+
function boundedStatusDescription(description) {
|
|
4624
|
+
return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
|
|
4625
|
+
}
|
|
4626
|
+
function gitLabStatusState(state) {
|
|
4627
|
+
switch (state) {
|
|
4628
|
+
case "pending": return "pending";
|
|
4629
|
+
case "success": return "success";
|
|
4630
|
+
case "failure": return "failed";
|
|
4631
|
+
case "neutral": return "skipped";
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
function projectWebUrl(mergeRequestUrl) {
|
|
4635
|
+
return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
|
|
4636
|
+
}
|
|
4637
|
+
//#endregion
|
|
3756
4638
|
//#region src/diff/git.ts
|
|
3757
4639
|
function runGit$1(args, cwd, maxBuffer) {
|
|
3758
4640
|
const result = Bun.spawnSync(["git", ...args], {
|
|
@@ -4114,32 +4996,217 @@ function createLocalChangeRequestEvent(options) {
|
|
|
4114
4996
|
});
|
|
4115
4997
|
}
|
|
4116
4998
|
//#endregion
|
|
4117
|
-
//#region src/
|
|
4118
|
-
const
|
|
4119
|
-
|
|
4120
|
-
|
|
4999
|
+
//#region src/diff/path-filter.ts
|
|
5000
|
+
const matcherOptions = {
|
|
5001
|
+
dot: true,
|
|
5002
|
+
nonegate: true,
|
|
5003
|
+
windows: false
|
|
5004
|
+
};
|
|
5005
|
+
const compiledFilters = /* @__PURE__ */ new WeakMap();
|
|
5006
|
+
function filterDiffManifestByPaths(manifest, filter) {
|
|
5007
|
+
if (!filter) return manifest;
|
|
5008
|
+
return {
|
|
5009
|
+
...manifest,
|
|
5010
|
+
files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
|
|
5011
|
+
};
|
|
4121
5012
|
}
|
|
4122
|
-
function
|
|
4123
|
-
|
|
5013
|
+
function diffFileMatchesPathFilter(file, filter) {
|
|
5014
|
+
if (!filter) return true;
|
|
5015
|
+
const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
|
|
5016
|
+
const compiled = readCompiledFilter(filter);
|
|
5017
|
+
const include = compiled.include;
|
|
5018
|
+
const exclude = compiled.exclude;
|
|
5019
|
+
const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
|
|
5020
|
+
const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
|
|
5021
|
+
return included && !excluded;
|
|
4124
5022
|
}
|
|
4125
|
-
function
|
|
4126
|
-
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
const
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
5023
|
+
function pathMatchesFilter(filePath, filter) {
|
|
5024
|
+
if (!filter) return true;
|
|
5025
|
+
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
5026
|
+
const compiled = readCompiledFilter(filter);
|
|
5027
|
+
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
5028
|
+
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
5029
|
+
}
|
|
5030
|
+
function readCompiledFilter(filter) {
|
|
5031
|
+
const existing = compiledFilters.get(filter);
|
|
5032
|
+
if (existing) return existing;
|
|
5033
|
+
const compiled = {
|
|
5034
|
+
include: filter.include?.map(compilePattern),
|
|
5035
|
+
exclude: filter.exclude?.map(compilePattern)
|
|
5036
|
+
};
|
|
5037
|
+
compiledFilters.set(filter, compiled);
|
|
5038
|
+
return compiled;
|
|
5039
|
+
}
|
|
5040
|
+
function compilePattern(pattern) {
|
|
5041
|
+
return {
|
|
5042
|
+
matches: picomatch(pattern, matcherOptions),
|
|
5043
|
+
matchBasename: !pattern.includes("/")
|
|
5044
|
+
};
|
|
5045
|
+
}
|
|
5046
|
+
//#endregion
|
|
5047
|
+
//#region src/diff/manifest-projection.ts
|
|
5048
|
+
const defaultDiffManifestPromptLimits = {
|
|
5049
|
+
fullMaxBytes: 128 * 1024,
|
|
5050
|
+
fullMaxEstimatedTokens: 32e3,
|
|
5051
|
+
condensedMaxBytes: 256 * 1024,
|
|
5052
|
+
condensedMaxEstimatedTokens: 64e3,
|
|
5053
|
+
toolResponseMaxBytes: 64 * 1024
|
|
5054
|
+
};
|
|
5055
|
+
function projectDiffManifest(manifest, options) {
|
|
5056
|
+
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
5057
|
+
const manifestOptions = options ?? {};
|
|
5058
|
+
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
5059
|
+
return parseDiffManifest({
|
|
5060
|
+
...scopedManifest,
|
|
5061
|
+
files: scopedManifest.files.map((file) => ({
|
|
5062
|
+
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
5063
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
5064
|
+
...rangeFieldsForOptions(range, manifestOptions),
|
|
5065
|
+
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
5066
|
+
}))
|
|
5067
|
+
}))
|
|
5068
|
+
});
|
|
5069
|
+
}
|
|
5070
|
+
function cloneDiffManifest(manifest) {
|
|
5071
|
+
return parseDiffManifest(structuredClone(manifest));
|
|
5072
|
+
}
|
|
5073
|
+
function prepareDiffManifestPrompt(manifest, config) {
|
|
5074
|
+
const limits = resolveDiffManifestPromptLimits(config);
|
|
5075
|
+
const full = measureDiffManifestPrompt(manifest);
|
|
5076
|
+
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
5077
|
+
mode: "full",
|
|
5078
|
+
manifest,
|
|
5079
|
+
metrics: {
|
|
5080
|
+
full,
|
|
5081
|
+
selected: full
|
|
5082
|
+
},
|
|
5083
|
+
limits
|
|
5084
|
+
};
|
|
5085
|
+
const condensedManifest = condenseDiffManifest(manifest);
|
|
5086
|
+
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
5087
|
+
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
5088
|
+
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
5089
|
+
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
5090
|
+
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
5091
|
+
].join("; "));
|
|
5092
|
+
return {
|
|
5093
|
+
mode: "condensed",
|
|
5094
|
+
manifest: condensedManifest,
|
|
5095
|
+
metrics: {
|
|
5096
|
+
full,
|
|
5097
|
+
selected: condensed
|
|
5098
|
+
},
|
|
5099
|
+
limits
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
function condenseDiffManifest(manifest) {
|
|
5103
|
+
return {
|
|
5104
|
+
baseSha: manifest.baseSha,
|
|
5105
|
+
headSha: manifest.headSha,
|
|
5106
|
+
mergeBaseSha: manifest.mergeBaseSha,
|
|
5107
|
+
files: manifest.files.map(condenseDiffManifestFile)
|
|
5108
|
+
};
|
|
5109
|
+
}
|
|
5110
|
+
function measureDiffManifestPrompt(manifest) {
|
|
5111
|
+
const json = JSON.stringify(manifest, null, 2);
|
|
5112
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
5113
|
+
return {
|
|
5114
|
+
bytes,
|
|
5115
|
+
estimatedTokens: Math.ceil(bytes / 4)
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
function resolveDiffManifestPromptLimits(config) {
|
|
5119
|
+
return {
|
|
5120
|
+
...defaultDiffManifestPromptLimits,
|
|
5121
|
+
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
5122
|
+
};
|
|
5123
|
+
}
|
|
5124
|
+
function manifestOptionsHaveEffect(options) {
|
|
5125
|
+
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
5126
|
+
}
|
|
5127
|
+
function withoutCompressedFileFields(file, compressed) {
|
|
5128
|
+
if (!compressed) return file;
|
|
5129
|
+
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
5130
|
+
return rest;
|
|
5131
|
+
}
|
|
5132
|
+
function withoutCompressedRangeFields(range, compressed) {
|
|
5133
|
+
if (!compressed) return range;
|
|
5134
|
+
const { summary: _summary, ...rest } = range;
|
|
5135
|
+
return rest;
|
|
5136
|
+
}
|
|
5137
|
+
function rangeFieldsForOptions(range, options) {
|
|
5138
|
+
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
5139
|
+
if (options.includePreviews === false) {
|
|
5140
|
+
const { preview: _preview, ...rest } = fields;
|
|
5141
|
+
return rest;
|
|
5142
|
+
}
|
|
5143
|
+
return fields;
|
|
5144
|
+
}
|
|
5145
|
+
function truncatePreview(preview, maxLines) {
|
|
5146
|
+
if (preview === void 0 || maxLines === void 0) return preview;
|
|
5147
|
+
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
5148
|
+
}
|
|
5149
|
+
function condenseDiffManifestFile(file) {
|
|
5150
|
+
return {
|
|
5151
|
+
path: file.path,
|
|
5152
|
+
previousPath: file.previousPath,
|
|
5153
|
+
status: file.status,
|
|
5154
|
+
language: file.language,
|
|
5155
|
+
additions: file.additions,
|
|
5156
|
+
deletions: file.deletions,
|
|
5157
|
+
hunks: file.hunks.map((hunk) => ({
|
|
5158
|
+
hunkIndex: hunk.hunkIndex,
|
|
5159
|
+
header: hunk.header,
|
|
5160
|
+
oldStart: hunk.oldStart,
|
|
5161
|
+
oldLines: hunk.oldLines,
|
|
5162
|
+
newStart: hunk.newStart,
|
|
5163
|
+
newLines: hunk.newLines,
|
|
5164
|
+
contentHash: hunk.contentHash
|
|
5165
|
+
})),
|
|
5166
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
5167
|
+
id: range.id,
|
|
5168
|
+
path: range.path,
|
|
5169
|
+
side: range.side,
|
|
5170
|
+
startLine: range.startLine,
|
|
5171
|
+
endLine: range.endLine,
|
|
5172
|
+
kind: range.kind,
|
|
5173
|
+
hunkIndex: range.hunkIndex,
|
|
5174
|
+
hunkHeader: range.hunkHeader,
|
|
5175
|
+
hunkContentHash: range.hunkContentHash
|
|
5176
|
+
})),
|
|
5177
|
+
excludedReason: file.excludedReason
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
5181
|
+
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
5182
|
+
}
|
|
5183
|
+
//#endregion
|
|
5184
|
+
//#region src/commands/grammar.ts
|
|
5185
|
+
const piprCommandPrefix = "@pipr";
|
|
5186
|
+
function firstNonEmptyLine(value) {
|
|
5187
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
5188
|
+
}
|
|
5189
|
+
function isPiprCommandLine(line) {
|
|
5190
|
+
return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
|
|
5191
|
+
}
|
|
5192
|
+
function parseCommandPattern(pattern, line) {
|
|
5193
|
+
const patternParts = commandPatternParts(pattern);
|
|
5194
|
+
const validationError = unsupportedCommandRestCaptureError(pattern);
|
|
5195
|
+
if (validationError) return {
|
|
5196
|
+
ok: false,
|
|
5197
|
+
error: validationError
|
|
5198
|
+
};
|
|
5199
|
+
const lineTokens = tokenizeCommandPattern(line);
|
|
5200
|
+
const captures = {};
|
|
5201
|
+
let index = 0;
|
|
5202
|
+
for (const part of patternParts) {
|
|
5203
|
+
if (isOptionalCommandPatternPart(part)) {
|
|
5204
|
+
const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
|
|
5205
|
+
if (nextIndex !== void 0) index = nextIndex;
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
if (isCommandRestCaptureToken(part)) {
|
|
5209
|
+
const value = lineTokens.slice(index).join(" ");
|
|
4143
5210
|
if (!value) return {
|
|
4144
5211
|
ok: false,
|
|
4145
5212
|
error: `Expected '${part}'`
|
|
@@ -4202,7 +5269,7 @@ function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
|
4202
5269
|
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
4203
5270
|
}
|
|
4204
5271
|
//#endregion
|
|
4205
|
-
//#region src/
|
|
5272
|
+
//#region src/host-run/entry-dispatch.ts
|
|
4206
5273
|
const permissionOrder = [
|
|
4207
5274
|
"read",
|
|
4208
5275
|
"triage",
|
|
@@ -4337,229 +5404,44 @@ function renderPlanCommandHelp(plan, reason) {
|
|
|
4337
5404
|
return lines.join("\n");
|
|
4338
5405
|
}
|
|
4339
5406
|
//#endregion
|
|
4340
|
-
//#region src/
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
};
|
|
4346
|
-
const
|
|
4347
|
-
|
|
4348
|
-
|
|
5407
|
+
//#region src/pi/runtime-tools.ts
|
|
5408
|
+
const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
|
|
5409
|
+
async function preparePiRuntimeReadTools(options) {
|
|
5410
|
+
const toolRoot = path.join(options.root, "runtime-tools");
|
|
5411
|
+
const baseRoot = path.join(toolRoot, "base");
|
|
5412
|
+
await mkdir(baseRoot, { recursive: true });
|
|
5413
|
+
const baseRanges = await materializeBaseRangeSnapshots({
|
|
5414
|
+
baseRoot,
|
|
5415
|
+
manifest: options.request.manifest,
|
|
5416
|
+
sourceWorkspace: options.sourceWorkspace,
|
|
5417
|
+
maxBytes: options.request.toolResponseMaxBytes
|
|
5418
|
+
});
|
|
5419
|
+
const data = {
|
|
5420
|
+
manifest: options.request.manifest,
|
|
5421
|
+
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
5422
|
+
baseRanges
|
|
5423
|
+
};
|
|
5424
|
+
const dataPath = path.join(toolRoot, "data.json");
|
|
5425
|
+
await Bun.write(dataPath, JSON.stringify(data));
|
|
4349
5426
|
return {
|
|
4350
|
-
|
|
4351
|
-
|
|
5427
|
+
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
5428
|
+
dataPath,
|
|
5429
|
+
toolNames: piRuntimeReadToolNames
|
|
4352
5430
|
};
|
|
4353
5431
|
}
|
|
4354
|
-
function
|
|
4355
|
-
|
|
4356
|
-
const
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
5432
|
+
async function piRuntimeToolsExtensionPath() {
|
|
5433
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5434
|
+
const candidates = [
|
|
5435
|
+
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
5436
|
+
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
5437
|
+
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
5438
|
+
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
5439
|
+
];
|
|
5440
|
+
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
5441
|
+
throw new Error("Unable to locate pipr runtime tools extension");
|
|
4363
5442
|
}
|
|
4364
|
-
function
|
|
4365
|
-
|
|
4366
|
-
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
4367
|
-
const compiled = readCompiledFilter(filter);
|
|
4368
|
-
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
4369
|
-
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
4370
|
-
}
|
|
4371
|
-
function readCompiledFilter(filter) {
|
|
4372
|
-
const existing = compiledFilters.get(filter);
|
|
4373
|
-
if (existing) return existing;
|
|
4374
|
-
const compiled = {
|
|
4375
|
-
include: filter.include?.map(compilePattern),
|
|
4376
|
-
exclude: filter.exclude?.map(compilePattern)
|
|
4377
|
-
};
|
|
4378
|
-
compiledFilters.set(filter, compiled);
|
|
4379
|
-
return compiled;
|
|
4380
|
-
}
|
|
4381
|
-
function compilePattern(pattern) {
|
|
4382
|
-
return {
|
|
4383
|
-
matches: picomatch(pattern, matcherOptions),
|
|
4384
|
-
matchBasename: !pattern.includes("/")
|
|
4385
|
-
};
|
|
4386
|
-
}
|
|
4387
|
-
//#endregion
|
|
4388
|
-
//#region src/diff/manifest-projection.ts
|
|
4389
|
-
const defaultDiffManifestPromptLimits = {
|
|
4390
|
-
fullMaxBytes: 128 * 1024,
|
|
4391
|
-
fullMaxEstimatedTokens: 32e3,
|
|
4392
|
-
condensedMaxBytes: 256 * 1024,
|
|
4393
|
-
condensedMaxEstimatedTokens: 64e3,
|
|
4394
|
-
toolResponseMaxBytes: 64 * 1024
|
|
4395
|
-
};
|
|
4396
|
-
function projectDiffManifest(manifest, options) {
|
|
4397
|
-
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
4398
|
-
const manifestOptions = options ?? {};
|
|
4399
|
-
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
4400
|
-
return parseDiffManifest({
|
|
4401
|
-
...scopedManifest,
|
|
4402
|
-
files: scopedManifest.files.map((file) => ({
|
|
4403
|
-
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
4404
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
4405
|
-
...rangeFieldsForOptions(range, manifestOptions),
|
|
4406
|
-
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
4407
|
-
}))
|
|
4408
|
-
}))
|
|
4409
|
-
});
|
|
4410
|
-
}
|
|
4411
|
-
function cloneDiffManifest(manifest) {
|
|
4412
|
-
return parseDiffManifest(structuredClone(manifest));
|
|
4413
|
-
}
|
|
4414
|
-
function prepareDiffManifestPrompt(manifest, config) {
|
|
4415
|
-
const limits = resolveDiffManifestPromptLimits(config);
|
|
4416
|
-
const full = measureDiffManifestPrompt(manifest);
|
|
4417
|
-
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
4418
|
-
mode: "full",
|
|
4419
|
-
manifest,
|
|
4420
|
-
metrics: {
|
|
4421
|
-
full,
|
|
4422
|
-
selected: full
|
|
4423
|
-
},
|
|
4424
|
-
limits
|
|
4425
|
-
};
|
|
4426
|
-
const condensedManifest = condenseDiffManifest(manifest);
|
|
4427
|
-
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
4428
|
-
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
4429
|
-
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
4430
|
-
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
4431
|
-
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
4432
|
-
].join("; "));
|
|
4433
|
-
return {
|
|
4434
|
-
mode: "condensed",
|
|
4435
|
-
manifest: condensedManifest,
|
|
4436
|
-
metrics: {
|
|
4437
|
-
full,
|
|
4438
|
-
selected: condensed
|
|
4439
|
-
},
|
|
4440
|
-
limits
|
|
4441
|
-
};
|
|
4442
|
-
}
|
|
4443
|
-
function condenseDiffManifest(manifest) {
|
|
4444
|
-
return {
|
|
4445
|
-
baseSha: manifest.baseSha,
|
|
4446
|
-
headSha: manifest.headSha,
|
|
4447
|
-
mergeBaseSha: manifest.mergeBaseSha,
|
|
4448
|
-
files: manifest.files.map(condenseDiffManifestFile)
|
|
4449
|
-
};
|
|
4450
|
-
}
|
|
4451
|
-
function measureDiffManifestPrompt(manifest) {
|
|
4452
|
-
const json = JSON.stringify(manifest, null, 2);
|
|
4453
|
-
const bytes = Buffer.byteLength(json, "utf8");
|
|
4454
|
-
return {
|
|
4455
|
-
bytes,
|
|
4456
|
-
estimatedTokens: Math.ceil(bytes / 4)
|
|
4457
|
-
};
|
|
4458
|
-
}
|
|
4459
|
-
function resolveDiffManifestPromptLimits(config) {
|
|
4460
|
-
return {
|
|
4461
|
-
...defaultDiffManifestPromptLimits,
|
|
4462
|
-
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
4463
|
-
};
|
|
4464
|
-
}
|
|
4465
|
-
function manifestOptionsHaveEffect(options) {
|
|
4466
|
-
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
4467
|
-
}
|
|
4468
|
-
function withoutCompressedFileFields(file, compressed) {
|
|
4469
|
-
if (!compressed) return file;
|
|
4470
|
-
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
4471
|
-
return rest;
|
|
4472
|
-
}
|
|
4473
|
-
function withoutCompressedRangeFields(range, compressed) {
|
|
4474
|
-
if (!compressed) return range;
|
|
4475
|
-
const { summary: _summary, ...rest } = range;
|
|
4476
|
-
return rest;
|
|
4477
|
-
}
|
|
4478
|
-
function rangeFieldsForOptions(range, options) {
|
|
4479
|
-
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
4480
|
-
if (options.includePreviews === false) {
|
|
4481
|
-
const { preview: _preview, ...rest } = fields;
|
|
4482
|
-
return rest;
|
|
4483
|
-
}
|
|
4484
|
-
return fields;
|
|
4485
|
-
}
|
|
4486
|
-
function truncatePreview(preview, maxLines) {
|
|
4487
|
-
if (preview === void 0 || maxLines === void 0) return preview;
|
|
4488
|
-
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
4489
|
-
}
|
|
4490
|
-
function condenseDiffManifestFile(file) {
|
|
4491
|
-
return {
|
|
4492
|
-
path: file.path,
|
|
4493
|
-
previousPath: file.previousPath,
|
|
4494
|
-
status: file.status,
|
|
4495
|
-
language: file.language,
|
|
4496
|
-
additions: file.additions,
|
|
4497
|
-
deletions: file.deletions,
|
|
4498
|
-
hunks: file.hunks.map((hunk) => ({
|
|
4499
|
-
hunkIndex: hunk.hunkIndex,
|
|
4500
|
-
header: hunk.header,
|
|
4501
|
-
oldStart: hunk.oldStart,
|
|
4502
|
-
oldLines: hunk.oldLines,
|
|
4503
|
-
newStart: hunk.newStart,
|
|
4504
|
-
newLines: hunk.newLines,
|
|
4505
|
-
contentHash: hunk.contentHash
|
|
4506
|
-
})),
|
|
4507
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
4508
|
-
id: range.id,
|
|
4509
|
-
path: range.path,
|
|
4510
|
-
side: range.side,
|
|
4511
|
-
startLine: range.startLine,
|
|
4512
|
-
endLine: range.endLine,
|
|
4513
|
-
kind: range.kind,
|
|
4514
|
-
hunkIndex: range.hunkIndex,
|
|
4515
|
-
hunkHeader: range.hunkHeader,
|
|
4516
|
-
hunkContentHash: range.hunkContentHash
|
|
4517
|
-
})),
|
|
4518
|
-
excludedReason: file.excludedReason
|
|
4519
|
-
};
|
|
4520
|
-
}
|
|
4521
|
-
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
4522
|
-
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
4523
|
-
}
|
|
4524
|
-
//#endregion
|
|
4525
|
-
//#region src/pi/runtime-tools.ts
|
|
4526
|
-
const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
|
|
4527
|
-
async function preparePiRuntimeReadTools(options) {
|
|
4528
|
-
const toolRoot = path.join(options.root, "runtime-tools");
|
|
4529
|
-
const baseRoot = path.join(toolRoot, "base");
|
|
4530
|
-
await mkdir(baseRoot, { recursive: true });
|
|
4531
|
-
const baseRanges = await materializeBaseRangeSnapshots({
|
|
4532
|
-
baseRoot,
|
|
4533
|
-
manifest: options.request.manifest,
|
|
4534
|
-
sourceWorkspace: options.sourceWorkspace,
|
|
4535
|
-
maxBytes: options.request.toolResponseMaxBytes
|
|
4536
|
-
});
|
|
4537
|
-
const data = {
|
|
4538
|
-
manifest: options.request.manifest,
|
|
4539
|
-
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
4540
|
-
baseRanges
|
|
4541
|
-
};
|
|
4542
|
-
const dataPath = path.join(toolRoot, "data.json");
|
|
4543
|
-
await Bun.write(dataPath, JSON.stringify(data));
|
|
4544
|
-
return {
|
|
4545
|
-
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
4546
|
-
dataPath,
|
|
4547
|
-
toolNames: piRuntimeReadToolNames
|
|
4548
|
-
};
|
|
4549
|
-
}
|
|
4550
|
-
async function piRuntimeToolsExtensionPath() {
|
|
4551
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
4552
|
-
const candidates = [
|
|
4553
|
-
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
4554
|
-
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
4555
|
-
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
4556
|
-
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
4557
|
-
];
|
|
4558
|
-
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
4559
|
-
throw new Error("Unable to locate pipr runtime tools extension");
|
|
4560
|
-
}
|
|
4561
|
-
async function pathExists(filePath) {
|
|
4562
|
-
return await Bun.file(filePath).exists();
|
|
5443
|
+
async function pathExists(filePath) {
|
|
5444
|
+
return await Bun.file(filePath).exists();
|
|
4563
5445
|
}
|
|
4564
5446
|
async function materializeBaseRangeSnapshots(options) {
|
|
4565
5447
|
const ranges = {};
|
|
@@ -4752,7 +5634,27 @@ const ignoredWorkspacePaths = /* @__PURE__ */ new Set([
|
|
|
4752
5634
|
".fallow",
|
|
4753
5635
|
"coverage"
|
|
4754
5636
|
]);
|
|
4755
|
-
const
|
|
5637
|
+
const typedEventSchema = z.looseObject({ type: z.string() });
|
|
5638
|
+
const piJsonEventTypes = /* @__PURE__ */ new Set([
|
|
5639
|
+
"agent_start",
|
|
5640
|
+
"agent_end",
|
|
5641
|
+
"auto_retry_start",
|
|
5642
|
+
"auto_retry_end",
|
|
5643
|
+
"compaction_start",
|
|
5644
|
+
"compaction_end",
|
|
5645
|
+
"message_start",
|
|
5646
|
+
"message_update",
|
|
5647
|
+
"message_end",
|
|
5648
|
+
"queue_update",
|
|
5649
|
+
"session",
|
|
5650
|
+
"session_info_changed",
|
|
5651
|
+
"thinking_level_changed",
|
|
5652
|
+
"tool_execution_start",
|
|
5653
|
+
"tool_execution_update",
|
|
5654
|
+
"tool_execution_end",
|
|
5655
|
+
"turn_start",
|
|
5656
|
+
"turn_end"
|
|
5657
|
+
]);
|
|
4756
5658
|
const tokenCountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
4757
5659
|
const assistantMessageEventSchema = z.looseObject({
|
|
4758
5660
|
type: z.literal("message_end"),
|
|
@@ -4770,6 +5672,14 @@ const assistantUsageMessageSchema = z.looseObject({
|
|
|
4770
5672
|
cost: z.looseObject({ total: z.number().nonnegative() })
|
|
4771
5673
|
})
|
|
4772
5674
|
});
|
|
5675
|
+
const defaultPiStreamLimits = {
|
|
5676
|
+
maxJsonEventBytes: 16 * 1024 * 1024,
|
|
5677
|
+
maxRawStdoutBytes: 16 * 1024 * 1024,
|
|
5678
|
+
maxStderrBytes: 16 * 1024 * 1024
|
|
5679
|
+
};
|
|
5680
|
+
const maxRetainedModelCount = 64;
|
|
5681
|
+
const maxRetainedModelBytes = 64 * 1024;
|
|
5682
|
+
const processTerminationGraceMs = 250;
|
|
4773
5683
|
async function runPi(options) {
|
|
4774
5684
|
const started = Date.now();
|
|
4775
5685
|
const sandbox = await createPiRunSandbox(options.workspace);
|
|
@@ -4786,19 +5696,13 @@ async function runPi(options) {
|
|
|
4786
5696
|
const promptPath = path.join(sandbox.root, "prompt.md");
|
|
4787
5697
|
await Bun.write(promptPath, options.prompt);
|
|
4788
5698
|
const args = buildPiArgs(options.provider, `@${promptPath}`, sandbox.sessionDir, preparedTools, options.builtinTools);
|
|
4789
|
-
|
|
5699
|
+
return await runProcess(options.piExecutable ?? "pi", args, {
|
|
4790
5700
|
cwd: sandbox.workspace,
|
|
4791
5701
|
env: buildPiEnv(options.provider, sandbox, options.env, preparedTools),
|
|
4792
5702
|
started,
|
|
4793
|
-
timeoutSeconds: options.timeoutSeconds
|
|
5703
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
5704
|
+
streamLimits: options.streamLimits ?? defaultPiStreamLimits
|
|
4794
5705
|
});
|
|
4795
|
-
const events = parsePiJsonEvents(result.stdout);
|
|
4796
|
-
return {
|
|
4797
|
-
...result,
|
|
4798
|
-
...events?.models.length ? { models: events.models } : {},
|
|
4799
|
-
...events?.usage ? { usage: events.usage } : {},
|
|
4800
|
-
stdout: result.exitCode === 0 ? events?.assistantText ?? result.stdout : result.stdout
|
|
4801
|
-
};
|
|
4802
5706
|
} finally {
|
|
4803
5707
|
await preparedTools?.custom?.close();
|
|
4804
5708
|
await chmodRecursive(sandbox.root, 493);
|
|
@@ -4920,69 +5824,195 @@ async function chmodRecursive(target, mode) {
|
|
|
4920
5824
|
const entries = await readdir(target, { withFileTypes: true });
|
|
4921
5825
|
for (const entry of entries) await chmodRecursive(path.join(target, entry.name), mode);
|
|
4922
5826
|
}
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5827
|
+
var PiOutputCollector = class {
|
|
5828
|
+
limits;
|
|
5829
|
+
mode = "undetermined";
|
|
5830
|
+
pending = "";
|
|
5831
|
+
pendingBytes = 0;
|
|
5832
|
+
rawOutput = "";
|
|
5833
|
+
rawOutputBytes = 0;
|
|
5834
|
+
failureReason;
|
|
5835
|
+
assistantText;
|
|
5836
|
+
models = [];
|
|
5837
|
+
modelSet = /* @__PURE__ */ new Set();
|
|
5838
|
+
modelBytes = 0;
|
|
5839
|
+
assistantMessageCount = 0;
|
|
5840
|
+
usageMessageCount = 0;
|
|
5841
|
+
inputTokens = 0;
|
|
5842
|
+
outputTokens = 0;
|
|
5843
|
+
costUsd = 0;
|
|
5844
|
+
usagePartial = false;
|
|
5845
|
+
stream = {
|
|
5846
|
+
rawStdoutBytes: 0,
|
|
5847
|
+
jsonEventCount: 0,
|
|
5848
|
+
largestEventBytes: 0,
|
|
5849
|
+
peakBufferedBytes: 0
|
|
5850
|
+
};
|
|
5851
|
+
constructor(limits) {
|
|
5852
|
+
this.limits = limits;
|
|
4932
5853
|
}
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
5854
|
+
push(chunk) {
|
|
5855
|
+
this.stream.rawStdoutBytes += Buffer$1.byteLength(chunk, "utf8");
|
|
5856
|
+
if (this.failureReason) return this.failureReason;
|
|
5857
|
+
let offset = 0;
|
|
5858
|
+
while (offset < chunk.length && !this.failureReason) {
|
|
5859
|
+
if (this.mode === "raw") {
|
|
5860
|
+
this.appendRaw(chunk.slice(offset));
|
|
5861
|
+
break;
|
|
5862
|
+
}
|
|
5863
|
+
const newline = chunk.indexOf("\n", offset);
|
|
5864
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
5865
|
+
this.appendPending(chunk.slice(offset, end));
|
|
5866
|
+
if (newline < 0 || this.failureReason) break;
|
|
5867
|
+
this.consumePending(true);
|
|
5868
|
+
offset = newline + 1;
|
|
5869
|
+
}
|
|
5870
|
+
return this.failureReason;
|
|
5871
|
+
}
|
|
5872
|
+
finish() {
|
|
5873
|
+
if (!this.failureReason && this.pending.length > 0) this.consumePending(false);
|
|
5874
|
+
if (this.failureReason) return {
|
|
5875
|
+
stdout: "",
|
|
5876
|
+
stream: this.stream
|
|
5877
|
+
};
|
|
5878
|
+
if (this.mode !== "json") return {
|
|
5879
|
+
stdout: this.rawOutput,
|
|
5880
|
+
stream: this.stream
|
|
5881
|
+
};
|
|
5882
|
+
return {
|
|
5883
|
+
stdout: this.assistantText ?? "",
|
|
5884
|
+
...this.models.length > 0 ? { models: this.models } : {},
|
|
5885
|
+
...this.usageMessageCount > 0 ? { usage: this.usage() } : {},
|
|
5886
|
+
stream: this.stream
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
failure() {
|
|
5890
|
+
return this.failureReason;
|
|
5891
|
+
}
|
|
5892
|
+
appendPending(fragment) {
|
|
5893
|
+
const fragmentBytes = Buffer$1.byteLength(fragment, "utf8");
|
|
5894
|
+
const nextBytes = this.pendingBytes + fragmentBytes;
|
|
5895
|
+
if (nextBytes > (this.mode === "json" ? this.limits.maxJsonEventBytes : Math.max(this.limits.maxJsonEventBytes, this.limits.maxRawStdoutBytes))) {
|
|
5896
|
+
this.fail(this.mode === "json" ? "Pi JSON event exceeded the output limit" : "Pi stdout exceeded the output limit");
|
|
5897
|
+
return;
|
|
5898
|
+
}
|
|
5899
|
+
this.pending += fragment;
|
|
5900
|
+
this.pendingBytes = nextBytes;
|
|
5901
|
+
this.recordPeak(this.pendingBytes);
|
|
5902
|
+
}
|
|
5903
|
+
consumePending(terminated) {
|
|
5904
|
+
const source = `${this.pending}${terminated ? "\n" : ""}`;
|
|
5905
|
+
const line = this.pending.trim();
|
|
5906
|
+
const eventBytes = Buffer$1.byteLength(line, "utf8");
|
|
5907
|
+
this.pending = "";
|
|
5908
|
+
this.pendingBytes = 0;
|
|
5909
|
+
if (!line) {
|
|
5910
|
+
if (this.mode === "undetermined") this.appendRaw(source);
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
const event = parsePiEvent(line);
|
|
5914
|
+
if (this.mode === "undetermined") {
|
|
5915
|
+
if (!event || !piJsonEventTypes.has(event.type)) {
|
|
5916
|
+
this.mode = "raw";
|
|
5917
|
+
this.appendRaw(source);
|
|
5918
|
+
return;
|
|
5919
|
+
}
|
|
5920
|
+
this.mode = "json";
|
|
5921
|
+
this.rawOutput = "";
|
|
5922
|
+
this.rawOutputBytes = 0;
|
|
5923
|
+
}
|
|
5924
|
+
if (!event) {
|
|
5925
|
+
this.fail("Pi JSON output was malformed");
|
|
5926
|
+
return;
|
|
5927
|
+
}
|
|
5928
|
+
if (eventBytes > this.limits.maxJsonEventBytes) {
|
|
5929
|
+
this.fail("Pi JSON event exceeded the output limit");
|
|
5930
|
+
return;
|
|
5931
|
+
}
|
|
5932
|
+
this.stream.jsonEventCount += 1;
|
|
5933
|
+
this.stream.largestEventBytes = Math.max(this.stream.largestEventBytes, eventBytes);
|
|
5934
|
+
this.consumeEvent(event);
|
|
5935
|
+
}
|
|
5936
|
+
appendRaw(value) {
|
|
5937
|
+
const valueBytes = Buffer$1.byteLength(value, "utf8");
|
|
5938
|
+
const nextBytes = this.rawOutputBytes + valueBytes;
|
|
5939
|
+
if (nextBytes > this.limits.maxRawStdoutBytes) {
|
|
5940
|
+
this.fail("Pi raw stdout exceeded the output limit");
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5943
|
+
this.rawOutput += value;
|
|
5944
|
+
this.rawOutputBytes = nextBytes;
|
|
5945
|
+
this.recordPeak(this.rawOutputBytes);
|
|
5946
|
+
}
|
|
5947
|
+
fail(reason) {
|
|
5948
|
+
this.failureReason = reason;
|
|
5949
|
+
this.pending = "";
|
|
5950
|
+
this.pendingBytes = 0;
|
|
5951
|
+
this.rawOutput = "";
|
|
5952
|
+
this.rawOutputBytes = 0;
|
|
5953
|
+
this.assistantText = void 0;
|
|
5954
|
+
this.models.length = 0;
|
|
5955
|
+
this.modelSet.clear();
|
|
5956
|
+
this.modelBytes = 0;
|
|
5957
|
+
}
|
|
5958
|
+
consumeEvent(event) {
|
|
5959
|
+
this.assistantText = assistantTextFromEvent(event) ?? this.assistantText;
|
|
4947
5960
|
const parsed = assistantMessageEventSchema.safeParse(event);
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
function assistantModels(messages) {
|
|
4952
|
-
const models = [];
|
|
4953
|
-
for (const message of messages) {
|
|
5961
|
+
if (!parsed.success) return;
|
|
5962
|
+
const message = parsed.data.message;
|
|
5963
|
+
this.assistantMessageCount += 1;
|
|
4954
5964
|
const model = message.responseModel ?? message.model;
|
|
4955
|
-
if (model
|
|
5965
|
+
if (model) {
|
|
5966
|
+
this.addModel(model);
|
|
5967
|
+
if (this.failureReason) return;
|
|
5968
|
+
}
|
|
5969
|
+
const usage = assistantUsageMessageSchema.safeParse(message);
|
|
5970
|
+
if (!usage.success) return;
|
|
5971
|
+
this.usageMessageCount += 1;
|
|
5972
|
+
this.addUsage(usage.data);
|
|
4956
5973
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5974
|
+
addModel(model) {
|
|
5975
|
+
if (this.modelSet.has(model)) return;
|
|
5976
|
+
const modelBytes = Buffer$1.byteLength(model, "utf8");
|
|
5977
|
+
if (this.modelSet.size >= maxRetainedModelCount || this.modelBytes + modelBytes > maxRetainedModelBytes) {
|
|
5978
|
+
this.fail("Pi model metadata exceeded the output limit");
|
|
5979
|
+
return;
|
|
5980
|
+
}
|
|
5981
|
+
this.modelSet.add(model);
|
|
5982
|
+
this.models.push(model);
|
|
5983
|
+
this.modelBytes += modelBytes;
|
|
5984
|
+
}
|
|
5985
|
+
addUsage(message) {
|
|
5986
|
+
const nextInputTokens = this.inputTokens + message.usage.input;
|
|
5987
|
+
if (Number.isSafeInteger(nextInputTokens)) this.inputTokens = nextInputTokens;
|
|
5988
|
+
else this.usagePartial = true;
|
|
5989
|
+
const nextOutputTokens = this.outputTokens + message.usage.output;
|
|
5990
|
+
if (Number.isSafeInteger(nextOutputTokens)) this.outputTokens = nextOutputTokens;
|
|
5991
|
+
else this.usagePartial = true;
|
|
5992
|
+
const nextCostUsd = this.costUsd + message.usage.cost.total;
|
|
5993
|
+
if (Number.isFinite(nextCostUsd)) this.costUsd = nextCostUsd;
|
|
5994
|
+
else this.usagePartial = true;
|
|
5995
|
+
}
|
|
5996
|
+
usage() {
|
|
5997
|
+
return {
|
|
5998
|
+
status: this.usagePartial || this.usageMessageCount !== this.assistantMessageCount ? "partial" : "complete",
|
|
5999
|
+
inputTokens: this.inputTokens,
|
|
6000
|
+
outputTokens: this.outputTokens,
|
|
6001
|
+
costUsd: this.costUsd
|
|
6002
|
+
};
|
|
6003
|
+
}
|
|
6004
|
+
recordPeak(bytes) {
|
|
6005
|
+
this.stream.peakBufferedBytes = Math.max(this.stream.peakBufferedBytes, bytes);
|
|
6006
|
+
}
|
|
6007
|
+
};
|
|
6008
|
+
function parsePiEvent(line) {
|
|
6009
|
+
try {
|
|
6010
|
+
const parsed = typedEventSchema.safeParse(JSON.parse(line));
|
|
6011
|
+
if (!parsed.success) return;
|
|
6012
|
+
return parsed.data;
|
|
6013
|
+
} catch {
|
|
6014
|
+
return;
|
|
4979
6015
|
}
|
|
4980
|
-
return {
|
|
4981
|
-
status: partial ? "partial" : "complete",
|
|
4982
|
-
inputTokens,
|
|
4983
|
-
outputTokens,
|
|
4984
|
-
costUsd
|
|
4985
|
-
};
|
|
4986
6016
|
}
|
|
4987
6017
|
function assistantTextFromEvent(event) {
|
|
4988
6018
|
if (event.type === "message_end" || event.type === "turn_end") return assistantMessageText(event.message);
|
|
@@ -5012,7 +6042,9 @@ function textContent(content) {
|
|
|
5012
6042
|
function runProcess(command, args, options) {
|
|
5013
6043
|
return new Promise((resolve, reject) => {
|
|
5014
6044
|
let timedOut = false;
|
|
6045
|
+
let streamFailure;
|
|
5015
6046
|
let timeout;
|
|
6047
|
+
let terminationTimeout;
|
|
5016
6048
|
const detached = process.platform !== "win32";
|
|
5017
6049
|
const child = spawn(command, args, {
|
|
5018
6050
|
cwd: options.cwd,
|
|
@@ -5024,34 +6056,85 @@ function runProcess(command, args, options) {
|
|
|
5024
6056
|
"pipe"
|
|
5025
6057
|
]
|
|
5026
6058
|
});
|
|
5027
|
-
|
|
6059
|
+
const stdout = new PiOutputCollector(options.streamLimits);
|
|
5028
6060
|
let stderr = "";
|
|
6061
|
+
let stderrBytes = 0;
|
|
5029
6062
|
child.stdout.setEncoding("utf8");
|
|
5030
6063
|
child.stderr.setEncoding("utf8");
|
|
5031
6064
|
child.stdout.on("data", (chunk) => {
|
|
5032
|
-
|
|
6065
|
+
const failure = stdout.push(chunk);
|
|
6066
|
+
if (failure) failStream(failure);
|
|
5033
6067
|
});
|
|
5034
6068
|
child.stderr.on("data", (chunk) => {
|
|
6069
|
+
if (streamFailure || timedOut) return;
|
|
6070
|
+
const nextBytes = stderrBytes + Buffer$1.byteLength(chunk, "utf8");
|
|
6071
|
+
if (nextBytes > options.streamLimits.maxStderrBytes) {
|
|
6072
|
+
failStream("Pi stderr exceeded the output limit");
|
|
6073
|
+
return;
|
|
6074
|
+
}
|
|
5035
6075
|
stderr += chunk;
|
|
6076
|
+
stderrBytes = nextBytes;
|
|
5036
6077
|
});
|
|
6078
|
+
const failStream = (reason) => {
|
|
6079
|
+
if (streamFailure || timedOut) return;
|
|
6080
|
+
streamFailure = reason;
|
|
6081
|
+
stderr = "";
|
|
6082
|
+
stderrBytes = 0;
|
|
6083
|
+
if (timeout) {
|
|
6084
|
+
clearTimeout(timeout);
|
|
6085
|
+
timeout = void 0;
|
|
6086
|
+
}
|
|
6087
|
+
terminateProcessGroup();
|
|
6088
|
+
};
|
|
6089
|
+
const terminateProcessGroup = () => {
|
|
6090
|
+
killProcessGroup(child, "SIGTERM");
|
|
6091
|
+
terminationTimeout ??= setTimeout(() => {
|
|
6092
|
+
killProcessGroup(child, "SIGKILL");
|
|
6093
|
+
}, processTerminationGraceMs);
|
|
6094
|
+
};
|
|
5037
6095
|
if (options.timeoutSeconds !== void 0) timeout = setTimeout(() => {
|
|
5038
6096
|
timedOut = true;
|
|
5039
|
-
|
|
6097
|
+
terminateProcessGroup();
|
|
5040
6098
|
}, options.timeoutSeconds * 1e3);
|
|
5041
6099
|
child.on("error", reject);
|
|
5042
6100
|
child.on("close", (exitCode) => {
|
|
5043
6101
|
if (timeout) clearTimeout(timeout);
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
stdout,
|
|
6102
|
+
resolve(finalizeProcessResult({
|
|
6103
|
+
collector: stdout,
|
|
5047
6104
|
stderr,
|
|
5048
|
-
exitCode
|
|
6105
|
+
exitCode,
|
|
6106
|
+
timedOut,
|
|
6107
|
+
streamFailure,
|
|
6108
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
5049
6109
|
durationMs: Date.now() - options.started
|
|
5050
|
-
});
|
|
6110
|
+
}));
|
|
5051
6111
|
});
|
|
5052
6112
|
});
|
|
5053
6113
|
}
|
|
5054
|
-
function
|
|
6114
|
+
function finalizeProcessResult(options) {
|
|
6115
|
+
const collected = options.collector.finish();
|
|
6116
|
+
const streamFailure = options.streamFailure ?? options.collector.failure();
|
|
6117
|
+
if (options.timedOut) return {
|
|
6118
|
+
...collected,
|
|
6119
|
+
stderr: `${options.stderr ? `${options.stderr}\n` : ""}Pi timed out after ${options.timeoutSeconds}s`,
|
|
6120
|
+
exitCode: 124,
|
|
6121
|
+
durationMs: options.durationMs
|
|
6122
|
+
};
|
|
6123
|
+
if (streamFailure) return {
|
|
6124
|
+
stdout: "",
|
|
6125
|
+
stderr: streamFailure,
|
|
6126
|
+
exitCode: 1,
|
|
6127
|
+
durationMs: options.durationMs,
|
|
6128
|
+
...collected.stream ? { stream: collected.stream } : {}
|
|
6129
|
+
};
|
|
6130
|
+
return {
|
|
6131
|
+
...collected,
|
|
6132
|
+
stderr: options.stderr,
|
|
6133
|
+
exitCode: options.exitCode ?? 1,
|
|
6134
|
+
durationMs: options.durationMs
|
|
6135
|
+
};
|
|
6136
|
+
}
|
|
6137
|
+
function killProcessGroup(child, signal) {
|
|
5055
6138
|
try {
|
|
5056
6139
|
if (process.platform !== "win32" && child.pid) {
|
|
5057
6140
|
process.kill(-child.pid, signal);
|
|
@@ -5063,19 +6146,13 @@ function killProcessGroup(child, signal) {
|
|
|
5063
6146
|
}
|
|
5064
6147
|
}
|
|
5065
6148
|
//#endregion
|
|
5066
|
-
//#region src/shared/redaction.ts
|
|
5067
|
-
const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
|
|
5068
|
-
function redactPotentialSecrets(value) {
|
|
5069
|
-
return value.replace(secretLikeTokenPattern, "[redacted secret]");
|
|
5070
|
-
}
|
|
5071
|
-
//#endregion
|
|
5072
6149
|
//#region src/shared/logging.ts
|
|
5073
6150
|
const sensitiveEnvNamePattern = /(TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL|COOKIE)/i;
|
|
5074
|
-
function
|
|
6151
|
+
function createRuntimeLog(options) {
|
|
5075
6152
|
const secrets = /* @__PURE__ */ new Set();
|
|
5076
6153
|
for (const [key, value] of Object.entries(options.env ?? process.env)) if (sensitiveEnvNamePattern.test(key)) addSecret(secrets, value);
|
|
5077
6154
|
const debugEnabled = (options.env ?? process.env).ACTIONS_STEP_DEBUG === "true" || (options.env ?? process.env).PIPR_LOG_LEVEL === "debug";
|
|
5078
|
-
const sink = options.logSink ??
|
|
6155
|
+
const sink = options.logSink ?? noopRuntimeLogSink;
|
|
5079
6156
|
return {
|
|
5080
6157
|
debugEnabled,
|
|
5081
6158
|
writesToSink: options.logSink !== void 0,
|
|
@@ -5152,7 +6229,7 @@ function emitRecord(sink, secrets, level, event, fields, text) {
|
|
|
5152
6229
|
text
|
|
5153
6230
|
});
|
|
5154
6231
|
}
|
|
5155
|
-
const
|
|
6232
|
+
const noopRuntimeLogSink = {
|
|
5156
6233
|
log() {},
|
|
5157
6234
|
async group(_name, run) {
|
|
5158
6235
|
return await run();
|
|
@@ -5677,7 +6754,8 @@ function logPiResult(options, provider, result, timeoutSeconds) {
|
|
|
5677
6754
|
durationMs: result.durationMs,
|
|
5678
6755
|
stdoutBytes: result.stdout.length,
|
|
5679
6756
|
stderrBytes: result.stderr.length,
|
|
5680
|
-
timeoutSeconds
|
|
6757
|
+
timeoutSeconds,
|
|
6758
|
+
...result.stream ?? {}
|
|
5681
6759
|
});
|
|
5682
6760
|
}
|
|
5683
6761
|
function builtinToolsForPrompt(toolMode) {
|
|
@@ -6085,6 +7163,7 @@ const inlinePublicationItemSchema = z.strictObject({
|
|
|
6085
7163
|
finding: reviewFindingSchema,
|
|
6086
7164
|
range: commentableRangeSchema,
|
|
6087
7165
|
path: z.string().min(1),
|
|
7166
|
+
previousPath: z.string().min(1).optional(),
|
|
6088
7167
|
side: reviewSideSchema,
|
|
6089
7168
|
startLine: z.number().int().positive(),
|
|
6090
7169
|
endLine: z.number().int().positive(),
|
|
@@ -6119,7 +7198,7 @@ const threadActionSchema = z.strictObject({
|
|
|
6119
7198
|
kind: z.enum(["resolve", "reply"]),
|
|
6120
7199
|
findingId: findingIdSchema,
|
|
6121
7200
|
findingHeadSha: z.string().min(1),
|
|
6122
|
-
commentId: z.
|
|
7201
|
+
commentId: z.string().min(1),
|
|
6123
7202
|
threadId: z.string().min(1).optional(),
|
|
6124
7203
|
body: z.string().min(1),
|
|
6125
7204
|
responseKey: z.string().min(1)
|
|
@@ -6148,6 +7227,20 @@ const publicationPlanSchema = z.strictObject({
|
|
|
6148
7227
|
reviewState: priorReviewStateSchema,
|
|
6149
7228
|
threadActions: threadActionsSchema
|
|
6150
7229
|
});
|
|
7230
|
+
function publicationPlanForHostCapabilities(plan, capabilities) {
|
|
7231
|
+
return {
|
|
7232
|
+
...plan,
|
|
7233
|
+
inlineItems: plan.inlineItems.filter((item) => capabilities.multilineInlineComments || item.startLine === item.endLine).map((item) => {
|
|
7234
|
+
if (capabilities.suggestedChanges || !item.finding.suggestedFix) return item;
|
|
7235
|
+
const finding = withoutSuggestedFix(item.finding);
|
|
7236
|
+
return {
|
|
7237
|
+
...item,
|
|
7238
|
+
finding,
|
|
7239
|
+
body: renderInlineBody(finding, item.findingId, item.reviewedHeadSha)
|
|
7240
|
+
};
|
|
7241
|
+
})
|
|
7242
|
+
};
|
|
7243
|
+
}
|
|
6151
7244
|
function buildPublicationPlan(options) {
|
|
6152
7245
|
const reviewState = options.reviewState ?? buildPriorReviewState({
|
|
6153
7246
|
findings: options.inlineItems.map((item) => item.finding),
|
|
@@ -6180,19 +7273,21 @@ function buildPublicationPlan(options) {
|
|
|
6180
7273
|
function preparePublishableInlineFindings(options) {
|
|
6181
7274
|
const ranges = createDiffRangeIndex(options.manifest);
|
|
6182
7275
|
return options.validated.validFindings.flatMap((finding) => {
|
|
6183
|
-
const
|
|
6184
|
-
if (!
|
|
7276
|
+
const match = ranges.findRange(finding.rangeId);
|
|
7277
|
+
if (!match) throw new Error(`Validated finding range '${finding.rangeId}' is missing from Diff Manifest`);
|
|
7278
|
+
const { file, range } = match;
|
|
6185
7279
|
const findingWithBody = findingWithPublishableBody(finding);
|
|
6186
7280
|
if (!findingWithBody) return [];
|
|
6187
7281
|
return [{
|
|
6188
7282
|
finding: findingWithPublishableSuggestedFix(findingWithBody, range),
|
|
6189
|
-
range
|
|
7283
|
+
range,
|
|
7284
|
+
previousPath: file.previousPath
|
|
6190
7285
|
}];
|
|
6191
7286
|
});
|
|
6192
7287
|
}
|
|
6193
7288
|
function prepareInlinePublicationItemsForPublishableFindings(options) {
|
|
6194
7289
|
const seenFindingIds = /* @__PURE__ */ new Set();
|
|
6195
|
-
return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range }) => {
|
|
7290
|
+
return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath }) => {
|
|
6196
7291
|
const findingId = findingIdFor(publishableFinding, options.reviewState);
|
|
6197
7292
|
const stateRecord = options.reviewState ? matchFindingRecord(options.reviewState, publishableFinding) : void 0;
|
|
6198
7293
|
if (seenFindingIds.has(findingId) || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
|
|
@@ -6202,6 +7297,7 @@ function prepareInlinePublicationItemsForPublishableFindings(options) {
|
|
|
6202
7297
|
finding: publishableFinding,
|
|
6203
7298
|
range,
|
|
6204
7299
|
path: publishableFinding.path,
|
|
7300
|
+
previousPath,
|
|
6205
7301
|
side: publishableFinding.side,
|
|
6206
7302
|
startLine: publishableFinding.startLine,
|
|
6207
7303
|
endLine: publishableFinding.endLine,
|
|
@@ -6604,8 +7700,12 @@ function internalVerifierAgent(provider, config) {
|
|
|
6604
7700
|
"Do not repeat user-supplied text unless needed to explain the verdict.",
|
|
6605
7701
|
config.publication.autoResolve.instructions
|
|
6606
7702
|
].filter(Boolean).join("\n"),
|
|
6607
|
-
prompt: (input) =>
|
|
7703
|
+
prompt: (input) => {
|
|
7704
|
+
const { manifest: _manifest, ...verifierPromptInput } = input;
|
|
7705
|
+
return JSON.stringify(verifierPromptInput, null, 2);
|
|
7706
|
+
},
|
|
6608
7707
|
tools: [],
|
|
7708
|
+
timeout: "2m",
|
|
6609
7709
|
retry: {
|
|
6610
7710
|
invalidOutput: 1,
|
|
6611
7711
|
transientFailure: 0
|
|
@@ -7163,106 +8263,1101 @@ function createTaskContext(options) {
|
|
|
7163
8263
|
secret(secret) {
|
|
7164
8264
|
return resolveTaskSecret(secret, options);
|
|
7165
8265
|
},
|
|
7166
|
-
pi: { async run(agent, input, runOptions) {
|
|
7167
|
-
const result = await runReviewAgent({
|
|
7168
|
-
agent,
|
|
7169
|
-
input,
|
|
7170
|
-
runOptions,
|
|
7171
|
-
runtime: {
|
|
7172
|
-
...options,
|
|
7173
|
-
taskContext,
|
|
7174
|
-
runId: options.runId,
|
|
7175
|
-
piRunSink: options.piRunSink
|
|
7176
|
-
}
|
|
7177
|
-
});
|
|
7178
|
-
options.output.providerModels.push(...result.providerModels);
|
|
7179
|
-
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
7180
|
-
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
7181
|
-
return agentOutputForTaskContext(agent, result.value);
|
|
7182
|
-
} },
|
|
7183
|
-
review: { async prior() {
|
|
7184
|
-
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
8266
|
+
pi: { async run(agent, input, runOptions) {
|
|
8267
|
+
const result = await runReviewAgent({
|
|
8268
|
+
agent,
|
|
8269
|
+
input,
|
|
8270
|
+
runOptions,
|
|
8271
|
+
runtime: {
|
|
8272
|
+
...options,
|
|
8273
|
+
taskContext,
|
|
8274
|
+
runId: options.runId,
|
|
8275
|
+
piRunSink: options.piRunSink
|
|
8276
|
+
}
|
|
8277
|
+
});
|
|
8278
|
+
options.output.providerModels.push(...result.providerModels);
|
|
8279
|
+
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
8280
|
+
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
8281
|
+
return agentOutputForTaskContext(agent, result.value);
|
|
8282
|
+
} },
|
|
8283
|
+
review: { async prior() {
|
|
8284
|
+
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
8285
|
+
} },
|
|
8286
|
+
check: createCheckHandle(options.output),
|
|
8287
|
+
async comment(value) {
|
|
8288
|
+
collectComment(options.output, value, options.taskName);
|
|
8289
|
+
},
|
|
8290
|
+
log: options.taskLog ?? console
|
|
8291
|
+
};
|
|
8292
|
+
return taskContext;
|
|
8293
|
+
}
|
|
8294
|
+
function agentOutputForTaskContext(_agent, value) {
|
|
8295
|
+
return value;
|
|
8296
|
+
}
|
|
8297
|
+
function resolveTaskSecret(secret, options) {
|
|
8298
|
+
if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
|
|
8299
|
+
const value = (options.env ?? process.env)[secret.name];
|
|
8300
|
+
if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
|
|
8301
|
+
options.log?.addSecret(value);
|
|
8302
|
+
return value;
|
|
8303
|
+
}
|
|
8304
|
+
function commandResponseRuntimeResult(options) {
|
|
8305
|
+
return {
|
|
8306
|
+
kind: "command-response",
|
|
8307
|
+
provider: options.provider,
|
|
8308
|
+
diffManifest: options.diffManifest,
|
|
8309
|
+
taskChecks: options.taskChecks,
|
|
8310
|
+
repairAttempted: options.output.repairAttempted,
|
|
8311
|
+
commandResponse: {
|
|
8312
|
+
commandName: options.commandInvocation.name,
|
|
8313
|
+
line: options.commandInvocation.line,
|
|
8314
|
+
arguments: options.commandInvocation.arguments,
|
|
8315
|
+
body: options.commandResponse.value
|
|
8316
|
+
}
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
8319
|
+
function skippedTaskRuntimeResult(options) {
|
|
8320
|
+
const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
|
|
8321
|
+
const review = {
|
|
8322
|
+
summary: { body: reason },
|
|
8323
|
+
inlineFindings: []
|
|
8324
|
+
};
|
|
8325
|
+
const validated = {
|
|
8326
|
+
review,
|
|
8327
|
+
validFindings: [],
|
|
8328
|
+
droppedFindings: []
|
|
8329
|
+
};
|
|
8330
|
+
const publicationPlan = buildCommentPublishingPlan({
|
|
8331
|
+
event: options.event,
|
|
8332
|
+
main: reason,
|
|
8333
|
+
validated,
|
|
8334
|
+
manifest: options.diffManifest,
|
|
8335
|
+
maxInlineComments: options.config.publication.maxInlineComments,
|
|
8336
|
+
showHeader: options.config.publication.showHeader,
|
|
8337
|
+
showFooter: options.config.publication.showFooter,
|
|
8338
|
+
showStats: options.config.publication.showStats,
|
|
8339
|
+
metadata: {
|
|
8340
|
+
runtimeVersion,
|
|
8341
|
+
configVersion: options.versionCompatibility?.configVersion,
|
|
8342
|
+
trustedConfigSha: options.trustedConfigSha,
|
|
8343
|
+
trustedConfigHash: options.trustedConfigHash,
|
|
8344
|
+
reviewedHeadSha: options.event.change.head.sha,
|
|
8345
|
+
providerModels: [options.provider.model],
|
|
8346
|
+
selectedTasks: [],
|
|
8347
|
+
failedTasks: [],
|
|
8348
|
+
validFindings: 0,
|
|
8349
|
+
droppedFindings: 0
|
|
8350
|
+
}
|
|
8351
|
+
}).publicationPlan;
|
|
8352
|
+
return {
|
|
8353
|
+
kind: "skipped",
|
|
8354
|
+
skipReason: reason,
|
|
8355
|
+
provider: options.provider,
|
|
8356
|
+
diffManifest: options.diffManifest,
|
|
8357
|
+
review,
|
|
8358
|
+
validated,
|
|
8359
|
+
publicationPlan,
|
|
8360
|
+
mainComment: publicationPlan.mainComment,
|
|
8361
|
+
inlineCommentDrafts: [],
|
|
8362
|
+
taskChecks: [],
|
|
8363
|
+
repairAttempted: false
|
|
8364
|
+
};
|
|
8365
|
+
}
|
|
8366
|
+
//#endregion
|
|
8367
|
+
//#region src/hosts/env.ts
|
|
8368
|
+
function requiredHostEnv(env, name, host) {
|
|
8369
|
+
const value = env[name];
|
|
8370
|
+
if (!value) throw new Error(`${name} is required for ${host} events`);
|
|
8371
|
+
return value;
|
|
8372
|
+
}
|
|
8373
|
+
function positiveIntegerHostEnv(env, name, host) {
|
|
8374
|
+
const value = Number(requiredHostEnv(env, name, host));
|
|
8375
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
|
8376
|
+
return value;
|
|
8377
|
+
}
|
|
8378
|
+
//#endregion
|
|
8379
|
+
//#region src/hosts/azure-devops/event.ts
|
|
8380
|
+
const repositorySchema = z.looseObject({
|
|
8381
|
+
id: z.string().min(1),
|
|
8382
|
+
name: z.string().min(1).optional(),
|
|
8383
|
+
project: z.looseObject({
|
|
8384
|
+
id: z.string().min(1),
|
|
8385
|
+
name: z.string().min(1)
|
|
8386
|
+
})
|
|
8387
|
+
});
|
|
8388
|
+
const serviceHookSchema = z.looseObject({
|
|
8389
|
+
id: z.string().min(1),
|
|
8390
|
+
eventType: z.string().min(1),
|
|
8391
|
+
resource: z.unknown(),
|
|
8392
|
+
resourceContainers: z.looseObject({
|
|
8393
|
+
account: z.looseObject({ baseUrl: z.string().url() }),
|
|
8394
|
+
project: z.looseObject({
|
|
8395
|
+
id: z.string().min(1),
|
|
8396
|
+
baseUrl: z.string().url()
|
|
8397
|
+
}).optional()
|
|
8398
|
+
})
|
|
8399
|
+
});
|
|
8400
|
+
const pullRequestResourceSchema = z.looseObject({
|
|
8401
|
+
pullRequestId: z.number().int().positive(),
|
|
8402
|
+
isDraft: z.boolean().optional(),
|
|
8403
|
+
repository: repositorySchema
|
|
8404
|
+
});
|
|
8405
|
+
const commentResourceSchema = z.looseObject({
|
|
8406
|
+
comment: z.looseObject({
|
|
8407
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
8408
|
+
parentCommentId: z.number().int().nonnegative().default(0),
|
|
8409
|
+
content: z.string(),
|
|
8410
|
+
author: z.looseObject({ uniqueName: z.string().min(1) })
|
|
8411
|
+
}),
|
|
8412
|
+
pullRequest: pullRequestResourceSchema
|
|
8413
|
+
});
|
|
8414
|
+
async function parseAzureDevOpsEvent(options) {
|
|
8415
|
+
return options.eventPath ? await serviceHookEvent(options) : await pipelineEvent$2(options);
|
|
8416
|
+
}
|
|
8417
|
+
async function pipelineEvent$2(options) {
|
|
8418
|
+
const organization = organizationFromCollectionUri(requiredHostEnv(options.env, "SYSTEM_COLLECTIONURI", "Azure DevOps pipeline"));
|
|
8419
|
+
const loaded = await loadChange$1(options, {
|
|
8420
|
+
organization,
|
|
8421
|
+
project: requiredHostEnv(options.env, "SYSTEM_TEAMPROJECT", "Azure DevOps pipeline"),
|
|
8422
|
+
repositoryId: requiredHostEnv(options.env, "BUILD_REPOSITORY_ID", "Azure DevOps pipeline"),
|
|
8423
|
+
changeNumber: positiveIntegerHostEnv(options.env, "SYSTEM_PULLREQUEST_PULLREQUESTID", "Azure DevOps pipeline")
|
|
8424
|
+
});
|
|
8425
|
+
if (loaded.change.isDraft) return draftEvent$1();
|
|
8426
|
+
return changeRequestEvent(loaded, {
|
|
8427
|
+
eventName: "azure_pipeline",
|
|
8428
|
+
action: options.env.PIPR_CHANGE_ACTION ?? "updated",
|
|
8429
|
+
rawAction: options.env.PIPR_CHANGE_ACTION,
|
|
8430
|
+
host: `https://dev.azure.com/${organization}`,
|
|
8431
|
+
workspace: options.workspace
|
|
8432
|
+
});
|
|
8433
|
+
}
|
|
8434
|
+
async function serviceHookEvent(options) {
|
|
8435
|
+
const payload = await Bun.file(options.eventPath ?? "").json();
|
|
8436
|
+
const hook = serviceHookSchema.parse(payload);
|
|
8437
|
+
const organization = organizationFromCollectionUri(hook.resourceContainers.account.baseUrl);
|
|
8438
|
+
if (hook.eventType === "ms.vss-code.git-pullrequest-comment-event") {
|
|
8439
|
+
const resource = commentResourceSchema.parse(hook.resource);
|
|
8440
|
+
const repository = resource.pullRequest.repository;
|
|
8441
|
+
const common = {
|
|
8442
|
+
eventName: hook.eventType,
|
|
8443
|
+
action: "created",
|
|
8444
|
+
rawAction: hook.eventType,
|
|
8445
|
+
repository: {
|
|
8446
|
+
slug: `${organization}/${repository.project.name}/${repository.name ?? repository.id}`,
|
|
8447
|
+
url: azureRepositoryUrl(organization, repository.project.name, repository.name ?? repository.id)
|
|
8448
|
+
},
|
|
8449
|
+
changeNumber: resource.pullRequest.pullRequestId,
|
|
8450
|
+
commentId: resource.comment.id,
|
|
8451
|
+
body: resource.comment.content,
|
|
8452
|
+
actor: resource.comment.author.uniqueName,
|
|
8453
|
+
workspace: options.workspace
|
|
8454
|
+
};
|
|
8455
|
+
return resource.comment.parentCommentId > 0 ? {
|
|
8456
|
+
kind: "review-comment-reply",
|
|
8457
|
+
reply: {
|
|
8458
|
+
...common,
|
|
8459
|
+
parentCommentId: String(resource.comment.parentCommentId)
|
|
8460
|
+
}
|
|
8461
|
+
} : {
|
|
8462
|
+
kind: "command-comment",
|
|
8463
|
+
comment: {
|
|
8464
|
+
...common,
|
|
8465
|
+
isChangeRequest: true
|
|
8466
|
+
}
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
if (hook.eventType === "git.pullrequest.created" || hook.eventType === "git.pullrequest.updated") {
|
|
8470
|
+
const resource = pullRequestResourceSchema.parse(hook.resource);
|
|
8471
|
+
return pullRequestHookEvent(options, organization, hook.eventType, resource);
|
|
8472
|
+
}
|
|
8473
|
+
throw new Error(`Unsupported Azure DevOps event '${hook.eventType}'`);
|
|
8474
|
+
}
|
|
8475
|
+
async function pullRequestHookEvent(options, organization, eventType, resource) {
|
|
8476
|
+
if (resource.isDraft) return draftEvent$1();
|
|
8477
|
+
return changeRequestEvent(await loadChange$1(options, {
|
|
8478
|
+
organization,
|
|
8479
|
+
project: resource.repository.project.name,
|
|
8480
|
+
repositoryId: resource.repository.id,
|
|
8481
|
+
changeNumber: resource.pullRequestId
|
|
8482
|
+
}), {
|
|
8483
|
+
eventName: eventType,
|
|
8484
|
+
action: eventType === "git.pullrequest.created" ? "opened" : "updated",
|
|
8485
|
+
rawAction: eventType,
|
|
8486
|
+
host: `https://dev.azure.com/${organization}`,
|
|
8487
|
+
workspace: options.workspace
|
|
8488
|
+
});
|
|
8489
|
+
}
|
|
8490
|
+
function draftEvent$1() {
|
|
8491
|
+
return {
|
|
8492
|
+
kind: "ignored",
|
|
8493
|
+
reason: "pull request is a draft"
|
|
8494
|
+
};
|
|
8495
|
+
}
|
|
8496
|
+
function changeRequestEvent(loaded, native) {
|
|
8497
|
+
return {
|
|
8498
|
+
kind: "change-request",
|
|
8499
|
+
change: parseChangeRequestEventContext({
|
|
8500
|
+
eventName: native.eventName,
|
|
8501
|
+
action: native.action,
|
|
8502
|
+
rawAction: native.rawAction,
|
|
8503
|
+
platform: {
|
|
8504
|
+
id: "azure-devops",
|
|
8505
|
+
host: native.host
|
|
8506
|
+
},
|
|
8507
|
+
repository: loaded.repository,
|
|
8508
|
+
coordinates: loaded.coordinates,
|
|
8509
|
+
change: loaded.change,
|
|
8510
|
+
workspace: native.workspace
|
|
8511
|
+
})
|
|
8512
|
+
};
|
|
8513
|
+
}
|
|
8514
|
+
async function loadChange$1(options, ref) {
|
|
8515
|
+
if (!options.loadChangeRequest) throw new Error("Azure DevOps pull request events require an API-backed change loader");
|
|
8516
|
+
return await options.loadChangeRequest(ref);
|
|
8517
|
+
}
|
|
8518
|
+
function organizationFromCollectionUri(value) {
|
|
8519
|
+
const organization = azureOrganizationFromUrl(value);
|
|
8520
|
+
if (!organization) throw new Error("Azure DevOps collection URI did not contain an organization");
|
|
8521
|
+
return organization;
|
|
8522
|
+
}
|
|
8523
|
+
function azureRepositoryUrl(organization, project, repository) {
|
|
8524
|
+
return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
|
|
8525
|
+
}
|
|
8526
|
+
//#endregion
|
|
8527
|
+
//#region src/review/publication-result.ts
|
|
8528
|
+
/** Error thrown when publication fails after producing partial result metadata. */
|
|
8529
|
+
var PublicationError = class extends Error {
|
|
8530
|
+
result;
|
|
8531
|
+
constructor(message, result) {
|
|
8532
|
+
super(message);
|
|
8533
|
+
this.result = result;
|
|
8534
|
+
}
|
|
8535
|
+
};
|
|
8536
|
+
//#endregion
|
|
8537
|
+
//#region src/hosts/publication.ts
|
|
8538
|
+
async function publishUnseenInlineItems(options) {
|
|
8539
|
+
const existing = new Set(extractInlineFindingMarkerRecords(options.existingBodies).map((record) => `${record.id}:${record.head}`));
|
|
8540
|
+
const errors = [];
|
|
8541
|
+
let posted = 0;
|
|
8542
|
+
let skipped = 0;
|
|
8543
|
+
for (const item of options.items) {
|
|
8544
|
+
if (existing.has(`${item.findingId}:${item.reviewedHeadSha}`)) {
|
|
8545
|
+
skipped += 1;
|
|
8546
|
+
continue;
|
|
8547
|
+
}
|
|
8548
|
+
try {
|
|
8549
|
+
await options.publish(item);
|
|
8550
|
+
posted += 1;
|
|
8551
|
+
} catch (error) {
|
|
8552
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
return {
|
|
8556
|
+
posted,
|
|
8557
|
+
skipped,
|
|
8558
|
+
errors
|
|
8559
|
+
};
|
|
8560
|
+
}
|
|
8561
|
+
function commandResponseBody(options) {
|
|
8562
|
+
const marker = `<!-- pipr:command-response change=${options.changeNumber} source=${options.sourceCommentId} command=${options.commandName} -->`;
|
|
8563
|
+
return {
|
|
8564
|
+
marker,
|
|
8565
|
+
body: [
|
|
8566
|
+
marker,
|
|
8567
|
+
"",
|
|
8568
|
+
options.body,
|
|
8569
|
+
""
|
|
8570
|
+
].join("\n")
|
|
8571
|
+
};
|
|
8572
|
+
}
|
|
8573
|
+
function completeHostPublication(options) {
|
|
8574
|
+
const partial = {
|
|
8575
|
+
inlineComments: {
|
|
8576
|
+
posted: options.inline.posted,
|
|
8577
|
+
skipped: options.inline.skipped,
|
|
8578
|
+
failed: options.inline.errors.length
|
|
8579
|
+
},
|
|
8580
|
+
metadata: {
|
|
8581
|
+
...options.metadata,
|
|
8582
|
+
inlinePublicationErrors: options.inline.errors,
|
|
8583
|
+
inlineResolutionErrors: options.resolutionErrors
|
|
8584
|
+
}
|
|
8585
|
+
};
|
|
8586
|
+
if (options.inline.errors.length > 0) throw new PublicationError(`${options.provider} inline comment publication failed`, partial);
|
|
8587
|
+
return {
|
|
8588
|
+
mainComment: {
|
|
8589
|
+
action: options.mainAction,
|
|
8590
|
+
id: options.mainId
|
|
8591
|
+
},
|
|
8592
|
+
...partial
|
|
8593
|
+
};
|
|
8594
|
+
}
|
|
8595
|
+
//#endregion
|
|
8596
|
+
//#region src/hosts/azure-devops/publication.ts
|
|
8597
|
+
async function publishAzureDevOpsPlan(options) {
|
|
8598
|
+
const coordinates = azureCoordinates$1(options.change);
|
|
8599
|
+
const native = await currentNativeChange(options.client, options.change);
|
|
8600
|
+
const owner = await options.client.currentUser();
|
|
8601
|
+
const threads = await options.client.listThreads(coordinates.repositoryId, options.change.change.number);
|
|
8602
|
+
const existingMain = ownedRootThread(threads, owner.uniqueName, mainMarker$2(options.change.change.number));
|
|
8603
|
+
const main = existingMain ? await options.client.updateComment(coordinates.repositoryId, options.change.change.number, existingMain.id, existingMain.comments[0]?.id ?? "", options.plan.mainComment) : (await options.client.createThread(coordinates.repositoryId, options.change.change.number, unpositionedThread(options.plan.mainComment))).comments[0];
|
|
8604
|
+
if (!main) throw new Error("Azure DevOps did not return the Main Review Comment");
|
|
8605
|
+
const markerBodies = ownedThreadComments(threads, owner.uniqueName).map((comment) => comment.content);
|
|
8606
|
+
const changes = await options.client.listIterationChanges(coordinates.repositoryId, options.change.change.number, native.iterationId);
|
|
8607
|
+
const inline = await publishUnseenInlineItems({
|
|
8608
|
+
items: options.plan.inlineItems,
|
|
8609
|
+
existingBodies: markerBodies,
|
|
8610
|
+
publish: async (item) => {
|
|
8611
|
+
await options.client.createThread(coordinates.repositoryId, options.change.change.number, await inlineThread(options.change, item, changes, native.iterationId));
|
|
8612
|
+
}
|
|
8613
|
+
});
|
|
8614
|
+
const resolution = await publishAzureDevOpsThreadActions({
|
|
8615
|
+
client: options.client,
|
|
8616
|
+
change: options.change,
|
|
8617
|
+
actions: options.plan.threadActions,
|
|
8618
|
+
reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
|
|
8619
|
+
threads
|
|
8620
|
+
});
|
|
8621
|
+
return completeHostPublication({
|
|
8622
|
+
provider: "Azure DevOps",
|
|
8623
|
+
mainAction: existingMain ? "updated" : "created",
|
|
8624
|
+
mainId: main.id,
|
|
8625
|
+
inline,
|
|
8626
|
+
resolutionErrors: resolution.errors,
|
|
8627
|
+
metadata: options.plan.metadata
|
|
8628
|
+
});
|
|
8629
|
+
}
|
|
8630
|
+
async function publishAzureDevOpsCommandResponse(options) {
|
|
8631
|
+
const coordinates = azureCoordinates$1(options.change);
|
|
8632
|
+
const response = commandResponseBody({
|
|
8633
|
+
changeNumber: options.change.change.number,
|
|
8634
|
+
sourceCommentId: options.sourceCommentId,
|
|
8635
|
+
commandName: options.commandName,
|
|
8636
|
+
body: options.body
|
|
8637
|
+
});
|
|
8638
|
+
await currentPullRequest(options.client, options.change);
|
|
8639
|
+
const owner = await options.client.currentUser();
|
|
8640
|
+
const existing = ownedRootThread(await options.client.listThreads(coordinates.repositoryId, options.change.change.number), owner.uniqueName, response.marker);
|
|
8641
|
+
const comment = existing ? await options.client.updateComment(coordinates.repositoryId, options.change.change.number, existing.id, existing.comments[0]?.id ?? "", response.body) : (await options.client.createThread(coordinates.repositoryId, options.change.change.number, unpositionedThread(response.body))).comments[0];
|
|
8642
|
+
if (!comment) throw new Error("Azure DevOps did not return the command response comment");
|
|
8643
|
+
return {
|
|
8644
|
+
action: existing ? "updated" : "created",
|
|
8645
|
+
id: comment.id
|
|
8646
|
+
};
|
|
8647
|
+
}
|
|
8648
|
+
async function loadAzureDevOpsPriorReviewState(options) {
|
|
8649
|
+
const state = extractPriorReviewState(await loadAzureDevOpsPriorMainComment(options), options.change.change.number);
|
|
8650
|
+
if (!state) return void 0;
|
|
8651
|
+
const owner = await options.client.currentUser();
|
|
8652
|
+
const bodies = ownedThreadComments(await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number), owner.uniqueName).map((comment) => comment.content);
|
|
8653
|
+
return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
|
|
8654
|
+
}
|
|
8655
|
+
async function loadAzureDevOpsPriorMainComment(options) {
|
|
8656
|
+
const owner = await options.client.currentUser();
|
|
8657
|
+
return ownedRootThread(await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number), owner.uniqueName, mainMarker$2(options.change.change.number))?.comments[0]?.content;
|
|
8658
|
+
}
|
|
8659
|
+
async function loadAzureDevOpsInlineThreadContexts(options) {
|
|
8660
|
+
const owner = await options.client.currentUser();
|
|
8661
|
+
return (await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number)).flatMap((thread) => {
|
|
8662
|
+
const root = thread.comments[0];
|
|
8663
|
+
const marker = root ? extractInlineFindingMarkerRecords([root.content])[0] : void 0;
|
|
8664
|
+
if (!root || !marker || root.author?.uniqueName !== owner.uniqueName) return [];
|
|
8665
|
+
return [{
|
|
8666
|
+
findingId: marker.id,
|
|
8667
|
+
findingHeadSha: marker.head,
|
|
8668
|
+
parentCommentId: root.id,
|
|
8669
|
+
parentBody: root.content,
|
|
8670
|
+
threadId: thread.id,
|
|
8671
|
+
threadResolved: isResolved(thread),
|
|
8672
|
+
comments: thread.comments.map((comment) => ({
|
|
8673
|
+
id: comment.id,
|
|
8674
|
+
body: comment.content,
|
|
8675
|
+
authorLogin: comment.author?.uniqueName
|
|
8676
|
+
}))
|
|
8677
|
+
}];
|
|
8678
|
+
});
|
|
8679
|
+
}
|
|
8680
|
+
async function publishAzureDevOpsThreadActions(options) {
|
|
8681
|
+
if (options.actions.length === 0) return { errors: [] };
|
|
8682
|
+
const coordinates = azureCoordinates$1(options.change);
|
|
8683
|
+
await currentNativeChange(options.client, options.change, options.reviewedHeadSha);
|
|
8684
|
+
const threads = options.threads ?? await options.client.listThreads(coordinates.repositoryId, options.change.change.number);
|
|
8685
|
+
const byComment = new Map(threads.flatMap((thread) => thread.comments.map((comment) => [comment.id, thread])));
|
|
8686
|
+
const errors = [];
|
|
8687
|
+
for (const action of options.actions) {
|
|
8688
|
+
const thread = action.threadId ? threads.find((candidate) => candidate.id === action.threadId) : byComment.get(action.commentId);
|
|
8689
|
+
const error = await publishAzureDevOpsThreadAction({
|
|
8690
|
+
client: options.client,
|
|
8691
|
+
repositoryId: coordinates.repositoryId,
|
|
8692
|
+
changeNumber: options.change.change.number,
|
|
8693
|
+
action,
|
|
8694
|
+
thread
|
|
8695
|
+
});
|
|
8696
|
+
if (error) errors.push(error);
|
|
8697
|
+
}
|
|
8698
|
+
return { errors };
|
|
8699
|
+
}
|
|
8700
|
+
async function publishAzureDevOpsThreadAction(options) {
|
|
8701
|
+
if (!options.thread) return `Azure DevOps thread not found for comment ${options.action.commentId}`;
|
|
8702
|
+
try {
|
|
8703
|
+
if (!options.thread.comments.some((comment) => comment.content.includes(options.action.responseKey))) await options.client.createThreadComment(options.repositoryId, options.changeNumber, options.thread.id, {
|
|
8704
|
+
parentCommentId: Number(options.thread.comments[0]?.id ?? 0),
|
|
8705
|
+
content: options.action.body,
|
|
8706
|
+
commentType: 1
|
|
8707
|
+
});
|
|
8708
|
+
if (options.action.kind === "resolve" && !isResolved(options.thread)) await options.client.updateThreadStatus(options.repositoryId, options.changeNumber, options.thread.id, "fixed");
|
|
8709
|
+
} catch (error) {
|
|
8710
|
+
return error instanceof Error ? error.message : String(error);
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8713
|
+
async function inlineThread(change, item, changes, iterationId) {
|
|
8714
|
+
const selectedPath = item.side === "LEFT" ? item.previousPath ?? item.path : item.path;
|
|
8715
|
+
const nativeChange = changes.find((candidate) => {
|
|
8716
|
+
if (!(candidate.path === selectedPath || candidate.originalPath === selectedPath)) return false;
|
|
8717
|
+
const changeType = candidate.changeType.toLowerCase();
|
|
8718
|
+
return item.side === "LEFT" ? changeType !== "add" : changeType !== "delete";
|
|
8719
|
+
});
|
|
8720
|
+
if (!nativeChange) throw new Error(`Azure DevOps changeTrackingId not found for ${selectedPath}`);
|
|
8721
|
+
const start = {
|
|
8722
|
+
line: item.startLine,
|
|
8723
|
+
offset: 1
|
|
8724
|
+
};
|
|
8725
|
+
const end = {
|
|
8726
|
+
line: item.endLine,
|
|
8727
|
+
offset: await lineEndOffset(change, selectedPath, item.endLine, item.side)
|
|
8728
|
+
};
|
|
8729
|
+
return {
|
|
8730
|
+
comments: [{
|
|
8731
|
+
parentCommentId: 0,
|
|
8732
|
+
content: item.body,
|
|
8733
|
+
commentType: 1
|
|
8734
|
+
}],
|
|
8735
|
+
status: "active",
|
|
8736
|
+
threadContext: {
|
|
8737
|
+
filePath: `/${selectedPath.replace(/^\/+/, "")}`,
|
|
8738
|
+
...item.side === "RIGHT" ? {
|
|
8739
|
+
rightFileStart: start,
|
|
8740
|
+
rightFileEnd: end
|
|
8741
|
+
} : {
|
|
8742
|
+
leftFileStart: start,
|
|
8743
|
+
leftFileEnd: end
|
|
8744
|
+
}
|
|
8745
|
+
},
|
|
8746
|
+
pullRequestThreadContext: {
|
|
8747
|
+
changeTrackingId: nativeChange.changeTrackingId,
|
|
8748
|
+
iterationContext: {
|
|
8749
|
+
firstComparingIteration: 1,
|
|
8750
|
+
secondComparingIteration: iterationId
|
|
8751
|
+
}
|
|
8752
|
+
}
|
|
8753
|
+
};
|
|
8754
|
+
}
|
|
8755
|
+
async function lineEndOffset(change, filePath, line, side) {
|
|
8756
|
+
const root = path.resolve(change.workspace);
|
|
8757
|
+
const sha = side === "RIGHT" ? change.change.head.sha : change.change.base.sha;
|
|
8758
|
+
try {
|
|
8759
|
+
const result = Bun.spawnSync([
|
|
8760
|
+
"git",
|
|
8761
|
+
"show",
|
|
8762
|
+
`${sha}:${filePath}`
|
|
8763
|
+
], { cwd: root });
|
|
8764
|
+
if (result.exitCode !== 0) return 1;
|
|
8765
|
+
const content = result.stdout.toString().split(/\r?\n/)[line - 1];
|
|
8766
|
+
return content === void 0 ? 1 : content.length + 1;
|
|
8767
|
+
} catch {
|
|
8768
|
+
return 1;
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
async function currentNativeChange(client, change, reviewedHeadSha = change.change.head.sha) {
|
|
8772
|
+
const pullRequest = await currentPullRequest(client, change, reviewedHeadSha);
|
|
8773
|
+
const coordinates = azureCoordinates$1(change);
|
|
8774
|
+
const iteration = (await client.listIterations(coordinates.repositoryId, change.change.number)).findLast((candidate) => candidate.headSha === reviewedHeadSha);
|
|
8775
|
+
if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${reviewedHeadSha}`);
|
|
8776
|
+
return {
|
|
8777
|
+
pullRequest,
|
|
8778
|
+
iterationId: iteration.id
|
|
8779
|
+
};
|
|
8780
|
+
}
|
|
8781
|
+
async function currentPullRequest(client, change, reviewedHeadSha = change.change.head.sha) {
|
|
8782
|
+
const coordinates = azureCoordinates$1(change);
|
|
8783
|
+
const pullRequest = await client.getPullRequest(coordinates.repositoryId, change.change.number);
|
|
8784
|
+
if (pullRequest.lastMergeSourceCommit.commitId !== reviewedHeadSha) throw new Error(`Azure DevOps pull request head changed from ${reviewedHeadSha} to ${pullRequest.lastMergeSourceCommit.commitId}`);
|
|
8785
|
+
if (pullRequest.lastMergeTargetCommit.commitId !== change.change.base.sha) throw new Error(`Azure DevOps pull request base changed from ${change.change.base.sha} to ${pullRequest.lastMergeTargetCommit.commitId}`);
|
|
8786
|
+
return pullRequest;
|
|
8787
|
+
}
|
|
8788
|
+
function azureCoordinates$1(change) {
|
|
8789
|
+
if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
|
|
8790
|
+
return change.coordinates;
|
|
8791
|
+
}
|
|
8792
|
+
function ownedRootThread(threads, uniqueName, marker) {
|
|
8793
|
+
return threads.find((thread) => {
|
|
8794
|
+
const root = thread.comments[0];
|
|
8795
|
+
return !thread.threadContext?.filePath && root?.author?.uniqueName === uniqueName && root.content.trimStart().startsWith(marker);
|
|
8796
|
+
});
|
|
8797
|
+
}
|
|
8798
|
+
function ownedThreadComments(threads, uniqueName) {
|
|
8799
|
+
return threads.flatMap((thread) => thread.comments.filter((comment) => comment.author?.uniqueName === uniqueName));
|
|
8800
|
+
}
|
|
8801
|
+
function unpositionedThread(content) {
|
|
8802
|
+
return {
|
|
8803
|
+
comments: [{
|
|
8804
|
+
parentCommentId: 0,
|
|
8805
|
+
content,
|
|
8806
|
+
commentType: 1
|
|
8807
|
+
}],
|
|
8808
|
+
status: "active"
|
|
8809
|
+
};
|
|
8810
|
+
}
|
|
8811
|
+
function isResolved(thread) {
|
|
8812
|
+
return thread.status === "fixed" || thread.status === "closed" || thread.status === "wontFix" || thread.status === "byDesign";
|
|
8813
|
+
}
|
|
8814
|
+
function mainMarker$2(changeNumber) {
|
|
8815
|
+
return `<!-- pipr:main-comment change=${changeNumber} `;
|
|
8816
|
+
}
|
|
8817
|
+
//#endregion
|
|
8818
|
+
//#region src/hosts/git.ts
|
|
8819
|
+
async function ensureCodeHostHeadCheckout(options) {
|
|
8820
|
+
if (!await hasGitCommit(options.rootDir, options.headSha)) {
|
|
8821
|
+
const remote = options.fetchRemote ?? "origin";
|
|
8822
|
+
try {
|
|
8823
|
+
await fetchGit(options.rootDir, remote, options.headSha, options.fetchEnv);
|
|
8824
|
+
} catch {
|
|
8825
|
+
await fetchGit(options.rootDir, remote, options.fetchRef, options.fetchEnv);
|
|
8826
|
+
}
|
|
8827
|
+
if (!await hasGitCommit(options.rootDir, options.headSha)) throw new Error(`Code host did not provide reviewed commit ${options.headSha} from ${options.fetchRef}`);
|
|
8828
|
+
}
|
|
8829
|
+
if ((await runGit(options.rootDir, ["rev-parse", "HEAD"])).trim() !== options.headSha) await runGit(options.rootDir, [
|
|
8830
|
+
"checkout",
|
|
8831
|
+
"--detach",
|
|
8832
|
+
options.headSha
|
|
8833
|
+
]);
|
|
8834
|
+
}
|
|
8835
|
+
async function fetchGit(rootDir, remote, ref, env) {
|
|
8836
|
+
await runGit(rootDir, [
|
|
8837
|
+
"fetch",
|
|
8838
|
+
"--no-tags",
|
|
8839
|
+
"--depth=1",
|
|
8840
|
+
remote,
|
|
8841
|
+
ref
|
|
8842
|
+
], env);
|
|
8843
|
+
}
|
|
8844
|
+
async function hasGitCommit(rootDir, sha) {
|
|
8845
|
+
try {
|
|
8846
|
+
await runGit(rootDir, [
|
|
8847
|
+
"cat-file",
|
|
8848
|
+
"-e",
|
|
8849
|
+
`${sha}^{commit}`
|
|
8850
|
+
]);
|
|
8851
|
+
return true;
|
|
8852
|
+
} catch {
|
|
8853
|
+
return false;
|
|
8854
|
+
}
|
|
8855
|
+
}
|
|
8856
|
+
async function runGit(rootDir, args, env) {
|
|
8857
|
+
const child = Bun.spawn(["git", ...args], {
|
|
8858
|
+
cwd: rootDir,
|
|
8859
|
+
env: {
|
|
8860
|
+
...process.env,
|
|
8861
|
+
...env
|
|
8862
|
+
},
|
|
8863
|
+
stderr: "pipe",
|
|
8864
|
+
stdout: "pipe"
|
|
8865
|
+
});
|
|
8866
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
8867
|
+
child.exited,
|
|
8868
|
+
new Response(child.stdout).text(),
|
|
8869
|
+
new Response(child.stderr).text()
|
|
8870
|
+
]);
|
|
8871
|
+
if (exitCode !== 0) throw new Error(stderr.trim() || `git ${args.join(" ")} failed`);
|
|
8872
|
+
return stdout;
|
|
8873
|
+
}
|
|
8874
|
+
//#endregion
|
|
8875
|
+
//#region src/hosts/azure-devops/workspace.ts
|
|
8876
|
+
function ensureAzureDevOpsHeadCheckout(options) {
|
|
8877
|
+
const headRef = options.change.change.head.ref;
|
|
8878
|
+
if (!headRef) throw new Error("Azure DevOps pull request source ref is required for checkout");
|
|
8879
|
+
return ensureCodeHostHeadCheckout({
|
|
8880
|
+
rootDir: options.rootDir,
|
|
8881
|
+
headSha: options.change.change.head.sha,
|
|
8882
|
+
fetchRef: headRef.startsWith("refs/") ? headRef : `refs/heads/${headRef}`,
|
|
8883
|
+
fetchRemote: options.change.change.isFork ? options.change.change.head.url : void 0
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
8886
|
+
//#endregion
|
|
8887
|
+
//#region src/hosts/azure-devops/adapter.ts
|
|
8888
|
+
function createAzureDevOpsHostAdapter(options = {}) {
|
|
8889
|
+
const client = options.client ?? createAzureDevOpsClient(options.env);
|
|
8890
|
+
return {
|
|
8891
|
+
id: "azure-devops",
|
|
8892
|
+
capabilities: {
|
|
8893
|
+
commandComments: true,
|
|
8894
|
+
reviewCommentReplies: true,
|
|
8895
|
+
threadResolution: true,
|
|
8896
|
+
multilineInlineComments: true,
|
|
8897
|
+
suggestedChanges: true,
|
|
8898
|
+
statuses: true
|
|
8899
|
+
},
|
|
8900
|
+
events: {
|
|
8901
|
+
parseEvent(parseOptions) {
|
|
8902
|
+
return parseAzureDevOpsEvent({
|
|
8903
|
+
...parseOptions,
|
|
8904
|
+
loadChangeRequest: (ref) => client.loadChange(ref)
|
|
8905
|
+
});
|
|
8906
|
+
},
|
|
8907
|
+
loadChangeRequest(ref) {
|
|
8908
|
+
const coordinates = azureCoordinatesFromRepository(client, ref.repository.slug);
|
|
8909
|
+
return client.loadChange({
|
|
8910
|
+
...coordinates,
|
|
8911
|
+
changeNumber: ref.changeNumber
|
|
8912
|
+
}).then((loaded) => ({
|
|
8913
|
+
...loaded,
|
|
8914
|
+
eventName: ref.eventName,
|
|
8915
|
+
action: ref.action,
|
|
8916
|
+
rawAction: ref.rawAction,
|
|
8917
|
+
workspace: ref.workspace
|
|
8918
|
+
}));
|
|
8919
|
+
}
|
|
8920
|
+
},
|
|
8921
|
+
workspace: { ensureHeadCheckout: ensureAzureDevOpsHeadCheckout },
|
|
8922
|
+
permissions: { getRepositoryPermission({ change, actor }) {
|
|
8923
|
+
const coordinates = azureCoordinates(change);
|
|
8924
|
+
if (!coordinates.projectId) throw new Error("Azure DevOps projectId is required for permission checks");
|
|
8925
|
+
return client.getRepositoryPermission(actor, coordinates.projectId, coordinates.repositoryId);
|
|
7185
8926
|
} },
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
8927
|
+
publication: {
|
|
8928
|
+
publish: ({ plan, change }) => publishAzureDevOpsPlan({
|
|
8929
|
+
client,
|
|
8930
|
+
plan,
|
|
8931
|
+
change
|
|
8932
|
+
}),
|
|
8933
|
+
publishCommandResponse: (args) => publishAzureDevOpsCommandResponse({
|
|
8934
|
+
client,
|
|
8935
|
+
...args
|
|
8936
|
+
}),
|
|
8937
|
+
publishThreadActions: (args) => publishAzureDevOpsThreadActions({
|
|
8938
|
+
client,
|
|
8939
|
+
...args
|
|
8940
|
+
})
|
|
7189
8941
|
},
|
|
7190
|
-
|
|
8942
|
+
comments: {
|
|
8943
|
+
loadPriorReviewState: ({ change }) => loadAzureDevOpsPriorReviewState({
|
|
8944
|
+
client,
|
|
8945
|
+
change
|
|
8946
|
+
}),
|
|
8947
|
+
loadPriorMainComment: ({ change }) => loadAzureDevOpsPriorMainComment({
|
|
8948
|
+
client,
|
|
8949
|
+
change
|
|
8950
|
+
}),
|
|
8951
|
+
loadInlineThreadContexts: ({ change }) => loadAzureDevOpsInlineThreadContexts({
|
|
8952
|
+
client,
|
|
8953
|
+
change
|
|
8954
|
+
})
|
|
8955
|
+
},
|
|
8956
|
+
statuses: {
|
|
8957
|
+
isAvailable: () => true,
|
|
8958
|
+
async upsert({ change, name, state, summary, status }) {
|
|
8959
|
+
const coordinates = azureCoordinates(change);
|
|
8960
|
+
const pullRequest = await client.getPullRequest(coordinates.repositoryId, change.change.number);
|
|
8961
|
+
if (pullRequest.lastMergeSourceCommit.commitId !== change.change.head.sha || pullRequest.lastMergeTargetCommit.commitId !== change.change.base.sha) throw new Error("Azure DevOps pull request endpoints changed before status publication");
|
|
8962
|
+
const iteration = (await client.listIterations(coordinates.repositoryId, change.change.number)).findLast((candidate) => candidate.headSha === change.change.head.sha);
|
|
8963
|
+
if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${change.change.head.sha}`);
|
|
8964
|
+
const id = await client.createStatus(coordinates.repositoryId, change.change.number, {
|
|
8965
|
+
state: azureDevOpsStatusState(state),
|
|
8966
|
+
description: summary?.slice(0, 1e3),
|
|
8967
|
+
context: {
|
|
8968
|
+
genre: "pipr",
|
|
8969
|
+
name: `pipr/${name}`
|
|
8970
|
+
},
|
|
8971
|
+
iterationId: iteration.id
|
|
8972
|
+
});
|
|
8973
|
+
return status ?? {
|
|
8974
|
+
id,
|
|
8975
|
+
name
|
|
8976
|
+
};
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
7191
8979
|
};
|
|
7192
|
-
return taskContext;
|
|
7193
8980
|
}
|
|
7194
|
-
function
|
|
7195
|
-
|
|
8981
|
+
function azureCoordinates(change) {
|
|
8982
|
+
if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
|
|
8983
|
+
return change.coordinates;
|
|
7196
8984
|
}
|
|
7197
|
-
function
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
8985
|
+
function azureCoordinatesFromRepository(client, slug) {
|
|
8986
|
+
const repositoryId = slug.split("/").at(-1);
|
|
8987
|
+
if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
|
|
8988
|
+
return {
|
|
8989
|
+
organization: client.organization,
|
|
8990
|
+
project: client.project,
|
|
8991
|
+
repositoryId
|
|
8992
|
+
};
|
|
7203
8993
|
}
|
|
7204
|
-
|
|
8994
|
+
//#endregion
|
|
8995
|
+
//#region src/hosts/bitbucket/event.ts
|
|
8996
|
+
const webhookSchema = z.looseObject({
|
|
8997
|
+
actor: z.looseObject({ nickname: z.string().min(1) }),
|
|
8998
|
+
repository: bitbucketRepositorySchema,
|
|
8999
|
+
pullrequest: z.looseObject({
|
|
9000
|
+
id: z.number().int().positive(),
|
|
9001
|
+
draft: z.boolean().optional()
|
|
9002
|
+
}),
|
|
9003
|
+
comment: z.looseObject({
|
|
9004
|
+
id: z.union([z.number(), z.string()]).transform(String),
|
|
9005
|
+
content: z.looseObject({ raw: z.string().default("") }),
|
|
9006
|
+
parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
|
|
9007
|
+
inline: z.unknown().optional()
|
|
9008
|
+
}).optional()
|
|
9009
|
+
});
|
|
9010
|
+
async function parseBitbucketEvent(options) {
|
|
9011
|
+
return options.eventPath ? await webhookEvent(options) : await pipelineEvent$1(options);
|
|
9012
|
+
}
|
|
9013
|
+
async function pipelineEvent$1(options) {
|
|
9014
|
+
const workspace = requiredHostEnv(options.env, "BITBUCKET_WORKSPACE", "Bitbucket");
|
|
9015
|
+
const repository = requiredHostEnv(options.env, "BITBUCKET_REPO_SLUG", "Bitbucket");
|
|
9016
|
+
const changeNumber = positiveIntegerHostEnv(options.env, "BITBUCKET_PR_ID", "Bitbucket");
|
|
9017
|
+
const loaded = await options.loadChangeRequest({
|
|
9018
|
+
workspace,
|
|
9019
|
+
repository,
|
|
9020
|
+
changeNumber
|
|
9021
|
+
});
|
|
9022
|
+
if (loaded.change.isDraft) return draftEvent();
|
|
7205
9023
|
return {
|
|
7206
|
-
kind: "
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
9024
|
+
kind: "change-request",
|
|
9025
|
+
change: {
|
|
9026
|
+
eventName: "bitbucket_pipeline",
|
|
9027
|
+
action: options.env.PIPR_CHANGE_ACTION ?? "updated",
|
|
9028
|
+
rawAction: options.env.PIPR_CHANGE_ACTION,
|
|
9029
|
+
platform: {
|
|
9030
|
+
id: "bitbucket",
|
|
9031
|
+
host: "https://bitbucket.org"
|
|
9032
|
+
},
|
|
9033
|
+
repository: loaded.repository,
|
|
9034
|
+
coordinates: loaded.coordinates,
|
|
9035
|
+
change: loaded.change,
|
|
9036
|
+
workspace: options.workspace
|
|
7216
9037
|
}
|
|
7217
9038
|
};
|
|
7218
9039
|
}
|
|
7219
|
-
function
|
|
7220
|
-
const
|
|
7221
|
-
const
|
|
7222
|
-
|
|
7223
|
-
|
|
9040
|
+
async function webhookEvent(options) {
|
|
9041
|
+
const hook = webhookSchema.parse(await Bun.file(options.eventPath ?? "").json());
|
|
9042
|
+
const eventKey = requiredHostEnv(options.env, "BITBUCKET_EVENT_KEY", "Bitbucket");
|
|
9043
|
+
if (eventKey === "pullrequest:comment_created") {
|
|
9044
|
+
if (!hook.comment) throw new Error("Bitbucket comment event payload is missing comment");
|
|
9045
|
+
return commentEvent(hook, hook.comment, eventKey, options.workspace);
|
|
9046
|
+
}
|
|
9047
|
+
return await pullRequestEvent(options, hook, eventKey);
|
|
9048
|
+
}
|
|
9049
|
+
function commentEvent(hook, comment, eventKey, workspace) {
|
|
9050
|
+
const common = {
|
|
9051
|
+
eventName: eventKey,
|
|
9052
|
+
action: "created",
|
|
9053
|
+
rawAction: eventKey,
|
|
9054
|
+
repository: {
|
|
9055
|
+
slug: hook.repository.full_name,
|
|
9056
|
+
url: hook.repository.links.html.href
|
|
9057
|
+
},
|
|
9058
|
+
changeNumber: hook.pullrequest.id,
|
|
9059
|
+
commentId: comment.id,
|
|
9060
|
+
body: comment.content.raw,
|
|
9061
|
+
actor: hook.actor.nickname,
|
|
9062
|
+
workspace
|
|
7224
9063
|
};
|
|
7225
|
-
|
|
7226
|
-
review,
|
|
7227
|
-
|
|
7228
|
-
|
|
9064
|
+
return comment.parent ? {
|
|
9065
|
+
kind: "review-comment-reply",
|
|
9066
|
+
reply: {
|
|
9067
|
+
...common,
|
|
9068
|
+
parentCommentId: comment.parent.id
|
|
9069
|
+
}
|
|
9070
|
+
} : {
|
|
9071
|
+
kind: "command-comment",
|
|
9072
|
+
comment: {
|
|
9073
|
+
...common,
|
|
9074
|
+
isChangeRequest: true
|
|
9075
|
+
}
|
|
7229
9076
|
};
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
9077
|
+
}
|
|
9078
|
+
async function pullRequestEvent(options, hook, eventKey) {
|
|
9079
|
+
const action = pullRequestAction(eventKey);
|
|
9080
|
+
if (hook.pullrequest.draft) return draftEvent();
|
|
9081
|
+
const loaded = await options.loadChangeRequest({
|
|
9082
|
+
workspace: hook.repository.full_name.split("/")[0] ?? "",
|
|
9083
|
+
repository: hook.repository.slug,
|
|
9084
|
+
changeNumber: hook.pullrequest.id
|
|
9085
|
+
});
|
|
9086
|
+
return {
|
|
9087
|
+
kind: "change-request",
|
|
9088
|
+
change: {
|
|
9089
|
+
eventName: eventKey,
|
|
9090
|
+
action,
|
|
9091
|
+
rawAction: eventKey,
|
|
9092
|
+
platform: {
|
|
9093
|
+
id: "bitbucket",
|
|
9094
|
+
host: "https://bitbucket.org"
|
|
9095
|
+
},
|
|
9096
|
+
repository: loaded.repository,
|
|
9097
|
+
coordinates: loaded.coordinates,
|
|
9098
|
+
change: loaded.change,
|
|
9099
|
+
workspace: options.workspace
|
|
7250
9100
|
}
|
|
7251
|
-
}
|
|
9101
|
+
};
|
|
9102
|
+
}
|
|
9103
|
+
function pullRequestAction(eventKey) {
|
|
9104
|
+
if (eventKey === "pullrequest:created") return "opened";
|
|
9105
|
+
if (eventKey === "pullrequest:updated") return "updated";
|
|
9106
|
+
if ([
|
|
9107
|
+
"pullrequest:fulfilled",
|
|
9108
|
+
"pullrequest:rejected",
|
|
9109
|
+
"pullrequest:superseded"
|
|
9110
|
+
].includes(eventKey)) return "closed";
|
|
9111
|
+
throw new Error(`Unsupported Bitbucket event: ${eventKey}`);
|
|
9112
|
+
}
|
|
9113
|
+
function draftEvent() {
|
|
7252
9114
|
return {
|
|
7253
|
-
kind: "
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
9115
|
+
kind: "ignored",
|
|
9116
|
+
reason: "pull request is a draft"
|
|
9117
|
+
};
|
|
9118
|
+
}
|
|
9119
|
+
//#endregion
|
|
9120
|
+
//#region src/hosts/bitbucket/publication.ts
|
|
9121
|
+
async function publishBitbucketPlan(options) {
|
|
9122
|
+
await assertCurrentEndpoints(options.client, options.change);
|
|
9123
|
+
const owner = await options.client.currentUser();
|
|
9124
|
+
const comments = await options.client.listComments(options.change.change.number);
|
|
9125
|
+
const owned = comments.filter((comment) => comment.user?.uuid === owner.uuid);
|
|
9126
|
+
const existingMain = owned.find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)));
|
|
9127
|
+
const main = existingMain ? await options.client.updateComment(options.change.change.number, existingMain.id, options.plan.mainComment) : await options.client.createComment(options.change.change.number, { content: { raw: options.plan.mainComment } });
|
|
9128
|
+
const inline = await publishUnseenInlineItems({
|
|
9129
|
+
items: options.plan.inlineItems,
|
|
9130
|
+
existingBodies: owned.map((comment) => comment.content.raw),
|
|
9131
|
+
publish: (item) => options.client.createComment(options.change.change.number, {
|
|
9132
|
+
content: { raw: item.body },
|
|
9133
|
+
inline: bitbucketInline(item)
|
|
9134
|
+
})
|
|
9135
|
+
});
|
|
9136
|
+
const resolution = await publishBitbucketThreadActions({
|
|
9137
|
+
client: options.client,
|
|
9138
|
+
change: options.change,
|
|
9139
|
+
actions: options.plan.threadActions,
|
|
9140
|
+
reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
|
|
9141
|
+
comments
|
|
9142
|
+
});
|
|
9143
|
+
return completeHostPublication({
|
|
9144
|
+
provider: "Bitbucket",
|
|
9145
|
+
mainAction: existingMain ? "updated" : "created",
|
|
9146
|
+
mainId: main.id,
|
|
9147
|
+
inline,
|
|
9148
|
+
resolutionErrors: resolution.errors,
|
|
9149
|
+
metadata: options.plan.metadata
|
|
9150
|
+
});
|
|
9151
|
+
}
|
|
9152
|
+
async function publishBitbucketCommandResponse(options) {
|
|
9153
|
+
const response = commandResponseBody({
|
|
9154
|
+
changeNumber: options.change.change.number,
|
|
9155
|
+
sourceCommentId: options.sourceCommentId,
|
|
9156
|
+
commandName: options.commandName,
|
|
9157
|
+
body: options.body
|
|
9158
|
+
});
|
|
9159
|
+
await assertCurrentEndpoints(options.client, options.change);
|
|
9160
|
+
const owner = await options.client.currentUser();
|
|
9161
|
+
const existing = (await options.client.listComments(options.change.change.number)).find((comment) => comment.user?.uuid === owner.uuid && comment.content.raw.includes(response.marker));
|
|
9162
|
+
const comment = existing ? await options.client.updateComment(options.change.change.number, existing.id, response.body) : await options.client.createComment(options.change.change.number, { content: { raw: response.body } });
|
|
9163
|
+
return {
|
|
9164
|
+
action: existing ? "updated" : "created",
|
|
9165
|
+
id: comment.id
|
|
9166
|
+
};
|
|
9167
|
+
}
|
|
9168
|
+
async function loadBitbucketPriorReviewState(options) {
|
|
9169
|
+
const comments = await loadBitbucketOwnedComments(options);
|
|
9170
|
+
const body = comments.find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)))?.content.raw;
|
|
9171
|
+
const state = extractPriorReviewState(body, options.change.change.number);
|
|
9172
|
+
if (!state) return void 0;
|
|
9173
|
+
const bodies = comments.map((comment) => comment.content.raw);
|
|
9174
|
+
return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
|
|
9175
|
+
}
|
|
9176
|
+
async function loadBitbucketPriorMainComment(options) {
|
|
9177
|
+
return (await loadBitbucketOwnedComments(options)).find((comment) => comment.content.raw.includes(mainMarker$1(options.change.change.number)))?.content.raw;
|
|
9178
|
+
}
|
|
9179
|
+
async function loadBitbucketOwnedComments(options) {
|
|
9180
|
+
const owner = await options.client.currentUser();
|
|
9181
|
+
return (await options.client.listComments(options.change.change.number)).filter((comment) => comment.user?.uuid === owner.uuid);
|
|
9182
|
+
}
|
|
9183
|
+
async function loadBitbucketInlineThreadContexts(options) {
|
|
9184
|
+
const owner = await options.client.currentUser();
|
|
9185
|
+
const comments = await options.client.listComments(options.change.change.number);
|
|
9186
|
+
return comments.flatMap((root) => {
|
|
9187
|
+
const marker = extractInlineFindingMarkerRecords([root.content.raw])[0];
|
|
9188
|
+
if (!marker || root.user?.uuid !== owner.uuid || root.parent) return [];
|
|
9189
|
+
const replies = comments.filter((comment) => comment.parent?.id === root.id);
|
|
9190
|
+
return [{
|
|
9191
|
+
findingId: marker.id,
|
|
9192
|
+
findingHeadSha: marker.head,
|
|
9193
|
+
parentCommentId: root.id,
|
|
9194
|
+
parentBody: root.content.raw,
|
|
9195
|
+
threadId: root.id,
|
|
9196
|
+
threadResolved: root.resolution !== void 0,
|
|
9197
|
+
comments: [root, ...replies].map((comment) => ({
|
|
9198
|
+
id: comment.id,
|
|
9199
|
+
body: comment.content.raw,
|
|
9200
|
+
authorLogin: comment.user?.nickname
|
|
9201
|
+
}))
|
|
9202
|
+
}];
|
|
9203
|
+
});
|
|
9204
|
+
}
|
|
9205
|
+
async function publishBitbucketThreadActions(options) {
|
|
9206
|
+
if (options.actions.length === 0) return { errors: [] };
|
|
9207
|
+
await assertCurrentEndpoints(options.client, options.change, options.reviewedHeadSha);
|
|
9208
|
+
const comments = options.comments ?? await options.client.listComments(options.change.change.number);
|
|
9209
|
+
const errors = [];
|
|
9210
|
+
for (const action of options.actions) {
|
|
9211
|
+
const root = comments.find((comment) => comment.id === (action.threadId ?? action.commentId) && !comment.parent);
|
|
9212
|
+
if (!root) {
|
|
9213
|
+
errors.push(`Bitbucket comment not found for ${action.commentId}`);
|
|
9214
|
+
continue;
|
|
9215
|
+
}
|
|
9216
|
+
try {
|
|
9217
|
+
if (!comments.filter((comment) => comment.parent?.id === root.id).some((comment) => comment.content.raw.includes(action.responseKey))) await options.client.replyToComment(options.change.change.number, root.id, action.body);
|
|
9218
|
+
if (action.kind === "resolve" && root.resolution === void 0) await options.client.resolveComment(options.change.change.number, root.id);
|
|
9219
|
+
} catch (error) {
|
|
9220
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
9221
|
+
}
|
|
9222
|
+
}
|
|
9223
|
+
return { errors };
|
|
9224
|
+
}
|
|
9225
|
+
function bitbucketInline(item) {
|
|
9226
|
+
return item.side === "RIGHT" ? {
|
|
9227
|
+
path: item.path,
|
|
9228
|
+
to: item.endLine,
|
|
9229
|
+
...item.startLine !== item.endLine ? { start_to: item.startLine } : {}
|
|
9230
|
+
} : {
|
|
9231
|
+
path: item.previousPath ?? item.path,
|
|
9232
|
+
from: item.endLine,
|
|
9233
|
+
...item.startLine !== item.endLine ? { start_from: item.startLine } : {}
|
|
9234
|
+
};
|
|
9235
|
+
}
|
|
9236
|
+
async function assertCurrentEndpoints(client, change, reviewedHeadSha = change.change.head.sha) {
|
|
9237
|
+
const pullRequest = await client.getPullRequest(change.change.number);
|
|
9238
|
+
if (pullRequest.source.commit.hash !== reviewedHeadSha || pullRequest.destination.commit.hash !== change.change.base.sha) throw new Error("Bitbucket pull request endpoints changed before publication");
|
|
9239
|
+
}
|
|
9240
|
+
function mainMarker$1(changeNumber) {
|
|
9241
|
+
return `<!-- pipr:main-comment change=${changeNumber} `;
|
|
9242
|
+
}
|
|
9243
|
+
//#endregion
|
|
9244
|
+
//#region src/hosts/bitbucket/workspace.ts
|
|
9245
|
+
function ensureBitbucketHeadCheckout(options) {
|
|
9246
|
+
const ref = options.change.change.head.ref;
|
|
9247
|
+
if (!ref) throw new Error("Bitbucket pull request source ref is required for checkout");
|
|
9248
|
+
const token = options.env?.BITBUCKET_API_TOKEN ?? process.env.BITBUCKET_API_TOKEN;
|
|
9249
|
+
const fetchEnv = options.change.change.isFork && token ? {
|
|
9250
|
+
...options.env,
|
|
9251
|
+
GIT_CONFIG_COUNT: "1",
|
|
9252
|
+
GIT_CONFIG_KEY_0: "http.extraHeader",
|
|
9253
|
+
GIT_CONFIG_VALUE_0: `Authorization: Basic ${Buffer.from(`x-bitbucket-api-token-auth:${token}`).toString("base64")}`
|
|
9254
|
+
} : options.env;
|
|
9255
|
+
return ensureCodeHostHeadCheckout({
|
|
9256
|
+
rootDir: options.rootDir,
|
|
9257
|
+
headSha: options.change.change.head.sha,
|
|
9258
|
+
fetchRef: ref.startsWith("refs/") ? ref : `refs/heads/${ref}`,
|
|
9259
|
+
fetchRemote: options.change.change.isFork ? options.change.change.head.url : void 0,
|
|
9260
|
+
fetchEnv
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
//#endregion
|
|
9264
|
+
//#region src/hosts/bitbucket/adapter.ts
|
|
9265
|
+
function createBitbucketHostAdapter(options = {}) {
|
|
9266
|
+
const client = options.client ?? createBitbucketClient(options.env);
|
|
9267
|
+
return {
|
|
9268
|
+
id: "bitbucket",
|
|
9269
|
+
capabilities: {
|
|
9270
|
+
commandComments: true,
|
|
9271
|
+
reviewCommentReplies: true,
|
|
9272
|
+
threadResolution: true,
|
|
9273
|
+
multilineInlineComments: true,
|
|
9274
|
+
suggestedChanges: false,
|
|
9275
|
+
statuses: true
|
|
9276
|
+
},
|
|
9277
|
+
events: {
|
|
9278
|
+
parseEvent: (parseOptions) => parseBitbucketEvent({
|
|
9279
|
+
...parseOptions,
|
|
9280
|
+
loadChangeRequest: (ref) => client.loadChange(ref)
|
|
9281
|
+
}),
|
|
9282
|
+
loadChangeRequest(ref) {
|
|
9283
|
+
const parts = ref.repository.slug.split("/");
|
|
9284
|
+
return client.loadChange({
|
|
9285
|
+
workspace: parts.at(-2) ?? client.workspace,
|
|
9286
|
+
repository: parts.at(-1) ?? client.repository,
|
|
9287
|
+
changeNumber: ref.changeNumber
|
|
9288
|
+
}).then((loaded) => ({
|
|
9289
|
+
...loaded,
|
|
9290
|
+
eventName: ref.eventName,
|
|
9291
|
+
action: ref.action,
|
|
9292
|
+
rawAction: ref.rawAction,
|
|
9293
|
+
workspace: ref.workspace
|
|
9294
|
+
}));
|
|
9295
|
+
}
|
|
9296
|
+
},
|
|
9297
|
+
workspace: { ensureHeadCheckout: (args) => ensureBitbucketHeadCheckout({
|
|
9298
|
+
...args,
|
|
9299
|
+
env: options.env
|
|
9300
|
+
}) },
|
|
9301
|
+
permissions: { getRepositoryPermission({ change, actor }) {
|
|
9302
|
+
const coordinates = bitbucketCoordinates(change);
|
|
9303
|
+
if (!coordinates.repositoryUuid) throw new Error("Bitbucket repository UUID is required for permission checks");
|
|
9304
|
+
return client.getRepositoryPermission(actor, coordinates.repositoryUuid);
|
|
9305
|
+
} },
|
|
9306
|
+
publication: {
|
|
9307
|
+
publish: ({ change, plan }) => publishBitbucketPlan({
|
|
9308
|
+
client,
|
|
9309
|
+
change,
|
|
9310
|
+
plan
|
|
9311
|
+
}),
|
|
9312
|
+
publishCommandResponse: (args) => publishBitbucketCommandResponse({
|
|
9313
|
+
client,
|
|
9314
|
+
...args
|
|
9315
|
+
}),
|
|
9316
|
+
publishThreadActions: (args) => publishBitbucketThreadActions({
|
|
9317
|
+
client,
|
|
9318
|
+
...args
|
|
9319
|
+
})
|
|
9320
|
+
},
|
|
9321
|
+
comments: {
|
|
9322
|
+
loadPriorReviewState: ({ change }) => loadBitbucketPriorReviewState({
|
|
9323
|
+
client,
|
|
9324
|
+
change
|
|
9325
|
+
}),
|
|
9326
|
+
loadPriorMainComment: ({ change }) => loadBitbucketPriorMainComment({
|
|
9327
|
+
client,
|
|
9328
|
+
change
|
|
9329
|
+
}),
|
|
9330
|
+
loadInlineThreadContexts: ({ change }) => loadBitbucketInlineThreadContexts({
|
|
9331
|
+
client,
|
|
9332
|
+
change
|
|
9333
|
+
})
|
|
9334
|
+
},
|
|
9335
|
+
statuses: {
|
|
9336
|
+
isAvailable: () => true,
|
|
9337
|
+
async upsert({ change, name, state, summary, status }) {
|
|
9338
|
+
const pullRequest = await client.getPullRequest(change.change.number);
|
|
9339
|
+
if (pullRequest.source.commit.hash !== change.change.head.sha || pullRequest.destination.commit.hash !== change.change.base.sha) throw new Error("Bitbucket pull request endpoints changed before status publication");
|
|
9340
|
+
const key = `pipr-${name}`.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
|
|
9341
|
+
const id = await client.setStatus(change.change.head.sha, key, {
|
|
9342
|
+
state: bitbucketStatusState(state),
|
|
9343
|
+
key,
|
|
9344
|
+
name: `Pipr: ${name}`.slice(0, 255),
|
|
9345
|
+
description: summary?.slice(0, 255),
|
|
9346
|
+
refname: change.change.head.ref,
|
|
9347
|
+
url: change.change.url
|
|
9348
|
+
});
|
|
9349
|
+
return status ?? {
|
|
9350
|
+
id,
|
|
9351
|
+
name
|
|
9352
|
+
};
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
7264
9355
|
};
|
|
7265
9356
|
}
|
|
9357
|
+
function bitbucketCoordinates(change) {
|
|
9358
|
+
if (change.coordinates?.provider !== "bitbucket") throw new Error("Bitbucket adapter requires Bitbucket coordinates");
|
|
9359
|
+
return change.coordinates;
|
|
9360
|
+
}
|
|
7266
9361
|
//#endregion
|
|
7267
9362
|
//#region src/shared/github.ts
|
|
7268
9363
|
const githubApiVersion = "2026-03-10";
|
|
@@ -7277,12 +9372,21 @@ function parseRepoSlug(value) {
|
|
|
7277
9372
|
repo
|
|
7278
9373
|
};
|
|
7279
9374
|
}
|
|
9375
|
+
function githubCoordinates(value) {
|
|
9376
|
+
const { owner, repo } = parseRepoSlug(value);
|
|
9377
|
+
return {
|
|
9378
|
+
provider: "github",
|
|
9379
|
+
owner,
|
|
9380
|
+
repository: repo
|
|
9381
|
+
};
|
|
9382
|
+
}
|
|
7280
9383
|
//#endregion
|
|
7281
9384
|
//#region src/hosts/github/command.ts
|
|
7282
9385
|
const pullRequestDetailsSchema = z.looseObject({
|
|
7283
9386
|
title: z.string().optional(),
|
|
7284
9387
|
body: z.string().nullable().optional(),
|
|
7285
9388
|
html_url: z.string().optional(),
|
|
9389
|
+
draft: z.boolean().optional(),
|
|
7286
9390
|
user: z.looseObject({ login: z.string().min(1).optional() }).optional(),
|
|
7287
9391
|
base: z.looseObject({
|
|
7288
9392
|
sha: z.string().min(1),
|
|
@@ -7305,7 +9409,7 @@ const pullRequestDetailsSchema = z.looseObject({
|
|
|
7305
9409
|
});
|
|
7306
9410
|
const githubRepositoryPermissionResponseSchema = z.looseObject({
|
|
7307
9411
|
permission: z.string().min(1),
|
|
7308
|
-
role_name: z.string().
|
|
9412
|
+
role_name: z.string().optional()
|
|
7309
9413
|
});
|
|
7310
9414
|
const permissionLevels = /* @__PURE__ */ new Set([
|
|
7311
9415
|
"read",
|
|
@@ -7375,7 +9479,8 @@ function githubPullRequestDetails(options) {
|
|
|
7375
9479
|
author: githubActor(options.json.head.user?.login),
|
|
7376
9480
|
fork: options.json.head.repo?.fork
|
|
7377
9481
|
},
|
|
7378
|
-
isFork: githubHeadIsFork(options.json, repository.slug)
|
|
9482
|
+
isFork: githubHeadIsFork(options.json, repository.slug),
|
|
9483
|
+
isDraft: options.json.draft
|
|
7379
9484
|
}
|
|
7380
9485
|
};
|
|
7381
9486
|
}
|
|
@@ -7402,6 +9507,7 @@ const githubPullRequestPayloadSchema = z.looseObject({
|
|
|
7402
9507
|
title: z.string().optional(),
|
|
7403
9508
|
body: z.string().nullable().optional(),
|
|
7404
9509
|
html_url: z.string().optional(),
|
|
9510
|
+
draft: z.boolean().optional(),
|
|
7405
9511
|
user: z.looseObject({ login: z.string().optional() }).optional(),
|
|
7406
9512
|
base: z.looseObject({
|
|
7407
9513
|
sha: z.string().optional(),
|
|
@@ -7462,7 +9568,7 @@ const githubIssueCommentEventContextSchema = z.strictObject({
|
|
|
7462
9568
|
url: z.string().min(1).optional()
|
|
7463
9569
|
}),
|
|
7464
9570
|
changeNumber: z.number({ error: "GitHub issue comment event is missing issue number" }).int().positive(),
|
|
7465
|
-
commentId: z.number({ error: "GitHub issue comment event is missing comment id" }).int().positive(),
|
|
9571
|
+
commentId: z.number({ error: "GitHub issue comment event is missing comment id" }).int().positive().transform(String),
|
|
7466
9572
|
isChangeRequest: z.boolean(),
|
|
7467
9573
|
body: z.string(),
|
|
7468
9574
|
actor: z.string({ error: "GitHub issue comment event is missing comment user" }).min(1),
|
|
@@ -7477,8 +9583,8 @@ const githubReviewCommentReplyEventSchema = z.strictObject({
|
|
|
7477
9583
|
url: z.string().min(1).optional()
|
|
7478
9584
|
}),
|
|
7479
9585
|
changeNumber: z.number({ error: "GitHub review comment event is missing pull request number" }).int().positive(),
|
|
7480
|
-
commentId: z.number({ error: "GitHub review comment event is missing comment id" }).int().positive(),
|
|
7481
|
-
parentCommentId: z.number().int().positive().optional(),
|
|
9586
|
+
commentId: z.number({ error: "GitHub review comment event is missing comment id" }).int().positive().transform(String),
|
|
9587
|
+
parentCommentId: z.number().int().positive().transform(String).optional(),
|
|
7482
9588
|
body: z.string(),
|
|
7483
9589
|
actor: z.string({ error: "GitHub review comment event is missing comment user" }).min(1),
|
|
7484
9590
|
workspace: z.string().min(1)
|
|
@@ -7497,18 +9603,21 @@ function githubPullRequestEventInput(payload, options) {
|
|
|
7497
9603
|
host: options.env.GITHUB_SERVER_URL
|
|
7498
9604
|
},
|
|
7499
9605
|
repository,
|
|
9606
|
+
coordinates: githubCoordinates(repository.slug),
|
|
7500
9607
|
change: githubEventChange(payload, repository.slug),
|
|
7501
9608
|
workspace: options.workspace
|
|
7502
9609
|
};
|
|
7503
9610
|
}
|
|
7504
9611
|
function githubEventRepository(payload, env) {
|
|
7505
9612
|
const baseRepo = payload.pull_request.base?.repo;
|
|
9613
|
+
const slug = [
|
|
9614
|
+
payload.repository?.full_name,
|
|
9615
|
+
baseRepo?.full_name,
|
|
9616
|
+
env.GITHUB_REPOSITORY
|
|
9617
|
+
].find((value) => value !== void 0 && value.length > 0);
|
|
9618
|
+
if (!slug) throw new Error("GitHub pull request event is missing repository coordinates");
|
|
7506
9619
|
return {
|
|
7507
|
-
slug
|
|
7508
|
-
payload.repository?.full_name,
|
|
7509
|
-
baseRepo?.full_name,
|
|
7510
|
-
env.GITHUB_REPOSITORY
|
|
7511
|
-
].find((value) => value !== void 0 && value.length > 0),
|
|
9620
|
+
slug,
|
|
7512
9621
|
url: payload.repository?.html_url ?? baseRepo?.html_url
|
|
7513
9622
|
};
|
|
7514
9623
|
}
|
|
@@ -7522,7 +9631,8 @@ function githubEventChange(payload, repositorySlug) {
|
|
|
7522
9631
|
author: githubActor(pullRequest.user?.login),
|
|
7523
9632
|
base: githubBaseEndpoint(pullRequest),
|
|
7524
9633
|
head: githubHeadEndpoint(pullRequest),
|
|
7525
|
-
isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug)
|
|
9634
|
+
isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug),
|
|
9635
|
+
isDraft: pullRequest.draft
|
|
7526
9636
|
};
|
|
7527
9637
|
}
|
|
7528
9638
|
function githubBaseEndpoint(pullRequest) {
|
|
@@ -7820,16 +9930,6 @@ const githubResolveReviewThreadMutation = `
|
|
|
7820
9930
|
}
|
|
7821
9931
|
`;
|
|
7822
9932
|
//#endregion
|
|
7823
|
-
//#region src/review/publication-result.ts
|
|
7824
|
-
/** Error thrown when publication fails after producing partial result metadata. */
|
|
7825
|
-
var PublicationError = class extends Error {
|
|
7826
|
-
result;
|
|
7827
|
-
constructor(message, result) {
|
|
7828
|
-
super(message);
|
|
7829
|
-
this.result = result;
|
|
7830
|
-
}
|
|
7831
|
-
};
|
|
7832
|
-
//#endregion
|
|
7833
9933
|
//#region src/hosts/github/publication-shared.ts
|
|
7834
9934
|
async function assertCurrentHeadSha(client, change, reviewedHeadSha) {
|
|
7835
9935
|
const headMismatch = await currentHeadShaMismatch(client, change, reviewedHeadSha);
|
|
@@ -7886,21 +9986,25 @@ async function publishGitHubCommandResponse(options) {
|
|
|
7886
9986
|
repo: options.change.repository.slug,
|
|
7887
9987
|
issueNumber: options.change.change.number
|
|
7888
9988
|
}), marker, ownerLogin);
|
|
7889
|
-
if (existing)
|
|
7890
|
-
|
|
7891
|
-
id: (await options.client.updateIssueComment({
|
|
9989
|
+
if (existing) {
|
|
9990
|
+
const updated = await options.client.updateIssueComment({
|
|
7892
9991
|
repo: options.change.repository.slug,
|
|
7893
9992
|
commentId: existing.id,
|
|
7894
9993
|
body
|
|
7895
|
-
})
|
|
7896
|
-
|
|
9994
|
+
});
|
|
9995
|
+
return {
|
|
9996
|
+
action: "updated",
|
|
9997
|
+
id: String(updated.id)
|
|
9998
|
+
};
|
|
9999
|
+
}
|
|
10000
|
+
const created = await options.client.createIssueComment({
|
|
10001
|
+
repo: options.change.repository.slug,
|
|
10002
|
+
issueNumber: options.change.change.number,
|
|
10003
|
+
body
|
|
10004
|
+
});
|
|
7897
10005
|
return {
|
|
7898
10006
|
action: "created",
|
|
7899
|
-
id: (
|
|
7900
|
-
repo: options.change.repository.slug,
|
|
7901
|
-
issueNumber: options.change.change.number,
|
|
7902
|
-
body
|
|
7903
|
-
})).id
|
|
10007
|
+
id: String(created.id)
|
|
7904
10008
|
};
|
|
7905
10009
|
}
|
|
7906
10010
|
function findCommandResponseComment(comments, marker, ownerLogin) {
|
|
@@ -7943,14 +10047,14 @@ async function loadGitHubInlineThreadContexts(options) {
|
|
|
7943
10047
|
return [{
|
|
7944
10048
|
findingId: marker.id,
|
|
7945
10049
|
findingHeadSha: marker.head,
|
|
7946
|
-
parentCommentId: comment.id,
|
|
10050
|
+
parentCommentId: String(comment.id),
|
|
7947
10051
|
parentBody: comment.body ?? "",
|
|
7948
10052
|
threadId: thread?.id,
|
|
7949
10053
|
threadResolved: thread?.isResolved ?? false,
|
|
7950
10054
|
comments: thread?.commentIds.flatMap((id) => {
|
|
7951
10055
|
const item = commentById.get(id);
|
|
7952
10056
|
return item ? [{
|
|
7953
|
-
id: item.id,
|
|
10057
|
+
id: String(item.id),
|
|
7954
10058
|
body: item.body ?? "",
|
|
7955
10059
|
authorLogin: item.authorLogin
|
|
7956
10060
|
}] : [];
|
|
@@ -8071,7 +10175,7 @@ async function postThreadActionReplyIfNeeded(context, action) {
|
|
|
8071
10175
|
await context.client.createReviewCommentReply({
|
|
8072
10176
|
repo: context.change.repository.slug,
|
|
8073
10177
|
pullRequestNumber: context.change.change.number,
|
|
8074
|
-
commentId: action.commentId,
|
|
10178
|
+
commentId: Number(action.commentId),
|
|
8075
10179
|
body: [
|
|
8076
10180
|
marker.body,
|
|
8077
10181
|
"",
|
|
@@ -8143,7 +10247,7 @@ async function resolveReviewThread(context, action) {
|
|
|
8143
10247
|
}
|
|
8144
10248
|
function threadForAction(context, action) {
|
|
8145
10249
|
if (action.kind !== "resolve") return;
|
|
8146
|
-
return (action.threadId ? context.threadById.get(action.threadId) : void 0) ?? context.threadByCommentId.get(action.commentId);
|
|
10250
|
+
return (action.threadId ? context.threadById.get(action.threadId) : void 0) ?? context.threadByCommentId.get(Number(action.commentId));
|
|
8147
10251
|
}
|
|
8148
10252
|
//#endregion
|
|
8149
10253
|
//#region src/hosts/github/publication-review.ts
|
|
@@ -8191,21 +10295,25 @@ async function upsertMainComment(options) {
|
|
|
8191
10295
|
repo: options.change.repository.slug,
|
|
8192
10296
|
issueNumber: options.change.change.number
|
|
8193
10297
|
}), options.plan.mainMarker, options.change.change.number, options.ownerLogin);
|
|
8194
|
-
if (existing)
|
|
8195
|
-
|
|
8196
|
-
id: (await options.client.updateIssueComment({
|
|
10298
|
+
if (existing) {
|
|
10299
|
+
const updated = await options.client.updateIssueComment({
|
|
8197
10300
|
repo: options.change.repository.slug,
|
|
8198
10301
|
commentId: existing.id,
|
|
8199
10302
|
body: options.plan.mainComment
|
|
8200
|
-
})
|
|
8201
|
-
|
|
10303
|
+
});
|
|
10304
|
+
return {
|
|
10305
|
+
action: "updated",
|
|
10306
|
+
id: String(updated.id)
|
|
10307
|
+
};
|
|
10308
|
+
}
|
|
10309
|
+
const created = await options.client.createIssueComment({
|
|
10310
|
+
repo: options.change.repository.slug,
|
|
10311
|
+
issueNumber: options.change.change.number,
|
|
10312
|
+
body: options.plan.mainComment
|
|
10313
|
+
});
|
|
8202
10314
|
return {
|
|
8203
10315
|
action: "created",
|
|
8204
|
-
id: (
|
|
8205
|
-
repo: options.change.repository.slug,
|
|
8206
|
-
issueNumber: options.change.change.number,
|
|
8207
|
-
body: options.plan.mainComment
|
|
8208
|
-
})).id
|
|
10316
|
+
id: String(created.id)
|
|
8209
10317
|
};
|
|
8210
10318
|
}
|
|
8211
10319
|
async function publishInlineComments(options) {
|
|
@@ -8361,48 +10469,18 @@ function gitConfigEnvCount(env) {
|
|
|
8361
10469
|
const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? process.env.GIT_CONFIG_COUNT ?? Bun.env.GIT_CONFIG_COUNT ?? "0", 10);
|
|
8362
10470
|
return Number.isSafeInteger(count) && count >= 0 ? count : 0;
|
|
8363
10471
|
}
|
|
8364
|
-
function gitGlobalConfigHome(env) {
|
|
8365
|
-
if (existsSync("/home/bun")) return "/home/bun";
|
|
8366
|
-
const root = env.RUNNER_TEMP ?? env.TMPDIR ?? os.tmpdir();
|
|
8367
|
-
mkdirSync(root, { recursive: true });
|
|
8368
|
-
return root;
|
|
8369
|
-
}
|
|
8370
|
-
function ensureGitHubHeadCheckout(options) {
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
"--depth=1",
|
|
8376
|
-
"origin",
|
|
8377
|
-
`refs/pull/${options.change.change.number}/head`
|
|
8378
|
-
]);
|
|
8379
|
-
if (runGit(options.rootDir, ["rev-parse", "HEAD"]).trim() !== headSha) runGit(options.rootDir, [
|
|
8380
|
-
"checkout",
|
|
8381
|
-
"--detach",
|
|
8382
|
-
headSha
|
|
8383
|
-
]);
|
|
8384
|
-
}
|
|
8385
|
-
function hasGitCommit(rootDir, sha) {
|
|
8386
|
-
try {
|
|
8387
|
-
runGit(rootDir, [
|
|
8388
|
-
"cat-file",
|
|
8389
|
-
"-e",
|
|
8390
|
-
`${sha}^{commit}`
|
|
8391
|
-
]);
|
|
8392
|
-
return true;
|
|
8393
|
-
} catch {
|
|
8394
|
-
return false;
|
|
8395
|
-
}
|
|
8396
|
-
}
|
|
8397
|
-
function runGit(rootDir, args) {
|
|
8398
|
-
const result = Bun.spawnSync(["git", ...args], {
|
|
8399
|
-
cwd: rootDir,
|
|
8400
|
-
env: process.env,
|
|
8401
|
-
stderr: "pipe",
|
|
8402
|
-
stdout: "pipe"
|
|
10472
|
+
function gitGlobalConfigHome(env) {
|
|
10473
|
+
if (existsSync("/home/bun")) return "/home/bun";
|
|
10474
|
+
const root = env.RUNNER_TEMP ?? env.TMPDIR ?? os.tmpdir();
|
|
10475
|
+
mkdirSync(root, { recursive: true });
|
|
10476
|
+
return root;
|
|
10477
|
+
}
|
|
10478
|
+
function ensureGitHubHeadCheckout(options) {
|
|
10479
|
+
return ensureCodeHostHeadCheckout({
|
|
10480
|
+
rootDir: options.rootDir,
|
|
10481
|
+
headSha: options.change.change.head.sha,
|
|
10482
|
+
fetchRef: `refs/pull/${options.change.change.number}/head`
|
|
8403
10483
|
});
|
|
8404
|
-
if (result.exitCode !== 0) throw new Error(result.stderr.toString().trim() || `git ${args.join(" ")} failed`);
|
|
8405
|
-
return result.stdout.toString();
|
|
8406
10484
|
}
|
|
8407
10485
|
//#endregion
|
|
8408
10486
|
//#region src/hosts/github/adapter.ts
|
|
@@ -8412,35 +10490,63 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8412
10490
|
const publicationClient = options.publicationClient ?? createGitHubPublicationClient(env);
|
|
8413
10491
|
return {
|
|
8414
10492
|
id: "github",
|
|
10493
|
+
capabilities: {
|
|
10494
|
+
commandComments: true,
|
|
10495
|
+
reviewCommentReplies: true,
|
|
10496
|
+
threadResolution: true,
|
|
10497
|
+
multilineInlineComments: true,
|
|
10498
|
+
suggestedChanges: true,
|
|
10499
|
+
statuses: true
|
|
10500
|
+
},
|
|
8415
10501
|
events: {
|
|
8416
|
-
parseEvent(parseOptions) {
|
|
8417
|
-
|
|
10502
|
+
async parseEvent(parseOptions) {
|
|
10503
|
+
const eventPath = parseOptions.eventPath;
|
|
10504
|
+
if (!eventPath) throw new Error("GITHUB_EVENT_PATH is required for GitHub events");
|
|
10505
|
+
const eventOptions = {
|
|
10506
|
+
...parseOptions,
|
|
10507
|
+
eventPath
|
|
10508
|
+
};
|
|
10509
|
+
if (parseOptions.env.GITHUB_EVENT_NAME === "issue_comment") return {
|
|
10510
|
+
kind: "command-comment",
|
|
10511
|
+
comment: await loadGitHubIssueCommentEventContext(eventOptions)
|
|
10512
|
+
};
|
|
10513
|
+
if (parseOptions.env.GITHUB_EVENT_NAME === "pull_request_review_comment") return {
|
|
10514
|
+
kind: "review-comment-reply",
|
|
10515
|
+
reply: await loadGitHubReviewCommentReplyEvent(eventOptions)
|
|
10516
|
+
};
|
|
10517
|
+
const change = await loadGitHubPullRequestEventContext(eventOptions);
|
|
10518
|
+
return change.change.isDraft ? {
|
|
10519
|
+
kind: "ignored",
|
|
10520
|
+
reason: "pull request is a draft"
|
|
10521
|
+
} : {
|
|
10522
|
+
kind: "change-request",
|
|
10523
|
+
change
|
|
10524
|
+
};
|
|
8418
10525
|
},
|
|
8419
10526
|
async loadChangeRequest(ref) {
|
|
10527
|
+
const loaded = await commandClient.getPullRequest({
|
|
10528
|
+
repository: ref.repository,
|
|
10529
|
+
changeNumber: ref.changeNumber
|
|
10530
|
+
});
|
|
8420
10531
|
return {
|
|
8421
|
-
...
|
|
8422
|
-
|
|
8423
|
-
changeNumber: ref.changeNumber
|
|
8424
|
-
}),
|
|
10532
|
+
...loaded,
|
|
10533
|
+
coordinates: githubCoordinates(loaded.repository.slug),
|
|
8425
10534
|
eventName: ref.eventName,
|
|
8426
10535
|
action: ref.action,
|
|
8427
10536
|
rawAction: ref.rawAction,
|
|
8428
10537
|
workspace: ref.workspace
|
|
8429
10538
|
};
|
|
8430
|
-
},
|
|
8431
|
-
resolveCommandComment(parseOptions) {
|
|
8432
|
-
return loadGitHubIssueCommentEventContext(parseOptions);
|
|
8433
|
-
},
|
|
8434
|
-
resolveReviewCommentReply(parseOptions) {
|
|
8435
|
-
return loadGitHubReviewCommentReplyEvent(parseOptions);
|
|
8436
10539
|
}
|
|
8437
10540
|
},
|
|
8438
10541
|
workspace: {
|
|
8439
10542
|
ensureHeadCheckout: ensureGitHubHeadCheckout,
|
|
8440
10543
|
ensureWorkspaceSafeDirectory: ensureGitHubWorkspaceSafeDirectory
|
|
8441
10544
|
},
|
|
8442
|
-
permissions: { getRepositoryPermission(
|
|
8443
|
-
return commandClient.getRepositoryPermission(
|
|
10545
|
+
permissions: { getRepositoryPermission({ change, actor }) {
|
|
10546
|
+
return commandClient.getRepositoryPermission({
|
|
10547
|
+
repository: change.repository,
|
|
10548
|
+
actor
|
|
10549
|
+
});
|
|
8444
10550
|
} },
|
|
8445
10551
|
publication: {
|
|
8446
10552
|
publish(options) {
|
|
@@ -8454,7 +10560,7 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8454
10560
|
return publishGitHubCommandResponse({
|
|
8455
10561
|
client: publicationClient,
|
|
8456
10562
|
change: options.change,
|
|
8457
|
-
sourceCommentId: options.sourceCommentId,
|
|
10563
|
+
sourceCommentId: Number(options.sourceCommentId),
|
|
8458
10564
|
commandName: options.commandName,
|
|
8459
10565
|
body: options.body
|
|
8460
10566
|
});
|
|
@@ -8488,44 +10594,509 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8488
10594
|
});
|
|
8489
10595
|
}
|
|
8490
10596
|
},
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
return
|
|
10597
|
+
statuses: {
|
|
10598
|
+
isAvailable(change) {
|
|
10599
|
+
return change.eventName === "pull_request";
|
|
10600
|
+
},
|
|
10601
|
+
async upsert(options) {
|
|
10602
|
+
if (!options.status) {
|
|
10603
|
+
const checkRun = await publicationClient.createCheckRun({
|
|
10604
|
+
repo: options.change.repository.slug,
|
|
10605
|
+
name: options.name,
|
|
10606
|
+
headSha: options.change.change.head.sha,
|
|
10607
|
+
summary: options.summary
|
|
10608
|
+
});
|
|
10609
|
+
return {
|
|
10610
|
+
id: String(checkRun.id),
|
|
10611
|
+
name: checkRun.name
|
|
10612
|
+
};
|
|
10613
|
+
}
|
|
10614
|
+
if (options.state === "pending") return options.status;
|
|
10615
|
+
await publicationClient.updateCheckRun({
|
|
8494
10616
|
repo: options.change.repository.slug,
|
|
8495
|
-
|
|
8496
|
-
|
|
10617
|
+
checkRunId: Number(options.status.id),
|
|
10618
|
+
name: options.status.name,
|
|
10619
|
+
conclusion: options.state,
|
|
8497
10620
|
summary: options.summary
|
|
8498
10621
|
});
|
|
10622
|
+
return options.status;
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
};
|
|
10626
|
+
}
|
|
10627
|
+
//#endregion
|
|
10628
|
+
//#region src/hosts/gitlab/event.ts
|
|
10629
|
+
const projectSchema = z.looseObject({
|
|
10630
|
+
id: z.union([z.number(), z.string()]),
|
|
10631
|
+
path_with_namespace: z.string().min(1),
|
|
10632
|
+
web_url: z.string().url().optional()
|
|
10633
|
+
});
|
|
10634
|
+
const mergeRequestHookSchema = z.looseObject({
|
|
10635
|
+
object_kind: z.literal("merge_request"),
|
|
10636
|
+
project: projectSchema,
|
|
10637
|
+
object_attributes: z.looseObject({
|
|
10638
|
+
iid: z.number().int().positive(),
|
|
10639
|
+
action: z.string().optional(),
|
|
10640
|
+
draft: z.boolean().optional()
|
|
10641
|
+
}),
|
|
10642
|
+
changes: z.looseObject({ draft: z.looseObject({
|
|
10643
|
+
previous: z.boolean(),
|
|
10644
|
+
current: z.boolean()
|
|
10645
|
+
}).optional() }).optional()
|
|
10646
|
+
});
|
|
10647
|
+
const noteHookSchema = z.looseObject({
|
|
10648
|
+
object_kind: z.literal("note"),
|
|
10649
|
+
project: projectSchema,
|
|
10650
|
+
merge_request: z.looseObject({ iid: z.number().int().positive() }),
|
|
10651
|
+
user: z.looseObject({ username: z.string().min(1) }),
|
|
10652
|
+
object_attributes: z.looseObject({
|
|
10653
|
+
id: z.union([z.number(), z.string()]),
|
|
10654
|
+
note: z.string(),
|
|
10655
|
+
action: z.string().optional(),
|
|
10656
|
+
noteable_type: z.string(),
|
|
10657
|
+
discussion_id: z.string().min(1).optional()
|
|
10658
|
+
})
|
|
10659
|
+
});
|
|
10660
|
+
async function parseGitLabEvent(options) {
|
|
10661
|
+
if (!options.eventPath) return await pipelineEvent(options);
|
|
10662
|
+
const payload = await Bun.file(options.eventPath).json();
|
|
10663
|
+
if (isObjectKind(payload, "merge_request")) return await mergeRequestEvent(payload, options);
|
|
10664
|
+
if (isObjectKind(payload, "note")) return await noteEvent(payload, options);
|
|
10665
|
+
throw new Error("Unsupported GitLab event payload");
|
|
10666
|
+
}
|
|
10667
|
+
async function pipelineEvent(options) {
|
|
10668
|
+
const loaded = await loadChange(options, {
|
|
10669
|
+
projectId: requiredHostEnv(options.env, "CI_PROJECT_ID", "GitLab pipeline"),
|
|
10670
|
+
projectPath: requiredHostEnv(options.env, "CI_PROJECT_PATH", "GitLab pipeline"),
|
|
10671
|
+
changeNumber: positiveIntegerHostEnv(options.env, "CI_MERGE_REQUEST_IID", "GitLab pipeline")
|
|
10672
|
+
});
|
|
10673
|
+
if (loaded.change.isDraft) return {
|
|
10674
|
+
kind: "ignored",
|
|
10675
|
+
reason: "merge request is a draft"
|
|
10676
|
+
};
|
|
10677
|
+
return {
|
|
10678
|
+
kind: "change-request",
|
|
10679
|
+
change: parseChangeRequestEventContext({
|
|
10680
|
+
eventName: "gitlab_pipeline",
|
|
10681
|
+
action: options.env.PIPR_CHANGE_ACTION ?? "updated",
|
|
10682
|
+
rawAction: options.env.PIPR_CHANGE_ACTION,
|
|
10683
|
+
platform: {
|
|
10684
|
+
id: "gitlab",
|
|
10685
|
+
host: options.env.CI_SERVER_URL ?? "https://gitlab.com"
|
|
8499
10686
|
},
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
10687
|
+
repository: loaded.repository,
|
|
10688
|
+
coordinates: loaded.coordinates,
|
|
10689
|
+
change: loaded.change,
|
|
10690
|
+
workspace: options.workspace
|
|
10691
|
+
})
|
|
10692
|
+
};
|
|
10693
|
+
}
|
|
10694
|
+
async function mergeRequestEvent(payload, options) {
|
|
10695
|
+
const hook = mergeRequestHookSchema.parse(payload);
|
|
10696
|
+
const becameReady = hook.changes?.draft?.previous === true && hook.changes.draft.current === false;
|
|
10697
|
+
if (hook.object_attributes.draft === true && !becameReady) return {
|
|
10698
|
+
kind: "ignored",
|
|
10699
|
+
reason: "merge request is a draft"
|
|
10700
|
+
};
|
|
10701
|
+
const loaded = await loadChange(options, {
|
|
10702
|
+
projectId: String(hook.project.id),
|
|
10703
|
+
projectPath: hook.project.path_with_namespace,
|
|
10704
|
+
changeNumber: hook.object_attributes.iid
|
|
10705
|
+
});
|
|
10706
|
+
return {
|
|
10707
|
+
kind: "change-request",
|
|
10708
|
+
change: parseChangeRequestEventContext({
|
|
10709
|
+
eventName: "merge_request",
|
|
10710
|
+
action: normalizeMergeRequestAction(hook.object_attributes.action, hook.changes?.draft),
|
|
10711
|
+
rawAction: hook.object_attributes.action,
|
|
10712
|
+
platform: {
|
|
10713
|
+
id: "gitlab",
|
|
10714
|
+
host: gitLabHost(hook.project.web_url)
|
|
10715
|
+
},
|
|
10716
|
+
repository: loaded.repository,
|
|
10717
|
+
coordinates: loaded.coordinates,
|
|
10718
|
+
change: loaded.change,
|
|
10719
|
+
workspace: options.workspace
|
|
10720
|
+
})
|
|
10721
|
+
};
|
|
10722
|
+
}
|
|
10723
|
+
async function noteEvent(payload, options) {
|
|
10724
|
+
const hook = noteHookSchema.parse(payload);
|
|
10725
|
+
if (hook.object_attributes.noteable_type !== "MergeRequest") throw new Error("GitLab note did not target a merge request");
|
|
10726
|
+
const common = {
|
|
10727
|
+
eventName: "note",
|
|
10728
|
+
action: hook.object_attributes.action === "create" ? "created" : hook.object_attributes.action,
|
|
10729
|
+
rawAction: hook.object_attributes.action,
|
|
10730
|
+
repository: {
|
|
10731
|
+
slug: hook.project.path_with_namespace,
|
|
10732
|
+
url: hook.project.web_url
|
|
10733
|
+
},
|
|
10734
|
+
changeNumber: hook.merge_request.iid,
|
|
10735
|
+
commentId: String(hook.object_attributes.id),
|
|
10736
|
+
body: hook.object_attributes.note,
|
|
10737
|
+
actor: hook.user.username,
|
|
10738
|
+
workspace: options.workspace
|
|
10739
|
+
};
|
|
10740
|
+
const parentCommentId = await options.resolveReplyParent?.({
|
|
10741
|
+
projectId: String(hook.project.id),
|
|
10742
|
+
changeNumber: hook.merge_request.iid,
|
|
10743
|
+
noteId: String(hook.object_attributes.id),
|
|
10744
|
+
discussionId: hook.object_attributes.discussion_id
|
|
10745
|
+
});
|
|
10746
|
+
return parentCommentId ? {
|
|
10747
|
+
kind: "review-comment-reply",
|
|
10748
|
+
reply: {
|
|
10749
|
+
...common,
|
|
10750
|
+
parentCommentId
|
|
10751
|
+
}
|
|
10752
|
+
} : {
|
|
10753
|
+
kind: "command-comment",
|
|
10754
|
+
comment: {
|
|
10755
|
+
...common,
|
|
10756
|
+
isChangeRequest: true
|
|
10757
|
+
}
|
|
10758
|
+
};
|
|
10759
|
+
}
|
|
10760
|
+
async function loadChange(options, ref) {
|
|
10761
|
+
if (!options.loadChangeRequest) throw new Error("GitLab merge request events require an API-backed change loader");
|
|
10762
|
+
return await options.loadChangeRequest(ref);
|
|
10763
|
+
}
|
|
10764
|
+
function normalizeMergeRequestAction(action, draftChange) {
|
|
10765
|
+
if (action === "update" && draftChange?.previous === true && draftChange.current === false) return "ready";
|
|
10766
|
+
return action ? gitLabMergeRequestActions[action] ?? action : void 0;
|
|
10767
|
+
}
|
|
10768
|
+
const gitLabMergeRequestActions = {
|
|
10769
|
+
open: "opened",
|
|
10770
|
+
reopen: "reopened",
|
|
10771
|
+
update: "updated",
|
|
10772
|
+
approved: "updated",
|
|
10773
|
+
unapproved: "updated",
|
|
10774
|
+
close: "closed",
|
|
10775
|
+
merge: "closed"
|
|
10776
|
+
};
|
|
10777
|
+
function isObjectKind(value, kind) {
|
|
10778
|
+
return typeof value === "object" && value !== null && "object_kind" in value && value.object_kind === kind;
|
|
10779
|
+
}
|
|
10780
|
+
function gitLabHost(projectUrl) {
|
|
10781
|
+
return projectUrl ? new URL(projectUrl).origin : "https://gitlab.com";
|
|
10782
|
+
}
|
|
10783
|
+
//#endregion
|
|
10784
|
+
//#region src/hosts/gitlab/publication.ts
|
|
10785
|
+
async function publishGitLabPlan(options) {
|
|
10786
|
+
const { projectId } = gitLabCoordinates$1(options.change);
|
|
10787
|
+
const owner = await options.client.currentUser();
|
|
10788
|
+
const existingMain = ownedNote(await options.client.listNotes(projectId, options.change.change.number), owner.username, mainMarker(options.change.change.number));
|
|
10789
|
+
const discussions = await options.client.listDiscussions(projectId, options.change.change.number);
|
|
10790
|
+
const ownedBodies = discussionNotes(discussions).filter((note) => note.author?.username === owner.username).map((note) => note.body);
|
|
10791
|
+
const mergeRequest = await assertCurrentHead(options.client, projectId, options.change);
|
|
10792
|
+
const main = existingMain ? await options.client.updateNote(projectId, options.change.change.number, existingMain.id, options.plan.mainComment) : await options.client.createNote(projectId, options.change.change.number, options.plan.mainComment);
|
|
10793
|
+
const inline = await publishUnseenInlineItems({
|
|
10794
|
+
items: options.plan.inlineItems,
|
|
10795
|
+
existingBodies: ownedBodies,
|
|
10796
|
+
publish: async (item) => {
|
|
10797
|
+
await options.client.createDiscussion(projectId, options.change.change.number, gitLabInlineBody(item), gitLabPosition(item, mergeRequest.diff_refs));
|
|
10798
|
+
}
|
|
10799
|
+
});
|
|
10800
|
+
const resolution = await publishGitLabThreadActions({
|
|
10801
|
+
client: options.client,
|
|
10802
|
+
change: options.change,
|
|
10803
|
+
actions: options.plan.threadActions,
|
|
10804
|
+
reviewedHeadSha: options.plan.metadata.reviewedHeadSha,
|
|
10805
|
+
discussions
|
|
10806
|
+
});
|
|
10807
|
+
return completeHostPublication({
|
|
10808
|
+
provider: "GitLab",
|
|
10809
|
+
mainAction: existingMain ? "updated" : "created",
|
|
10810
|
+
mainId: main.id,
|
|
10811
|
+
inline,
|
|
10812
|
+
resolutionErrors: resolution.errors,
|
|
10813
|
+
metadata: options.plan.metadata
|
|
10814
|
+
});
|
|
10815
|
+
}
|
|
10816
|
+
async function publishGitLabCommandResponse(options) {
|
|
10817
|
+
const { projectId } = gitLabCoordinates$1(options.change);
|
|
10818
|
+
const owner = await options.client.currentUser();
|
|
10819
|
+
const response = commandResponseBody({
|
|
10820
|
+
changeNumber: options.change.change.number,
|
|
10821
|
+
sourceCommentId: options.sourceCommentId,
|
|
10822
|
+
commandName: options.commandName,
|
|
10823
|
+
body: options.body
|
|
10824
|
+
});
|
|
10825
|
+
const existing = ownedNote(await options.client.listNotes(projectId, options.change.change.number), owner.username, response.marker);
|
|
10826
|
+
await assertCurrentHead(options.client, projectId, options.change);
|
|
10827
|
+
const note = existing ? await options.client.updateNote(projectId, options.change.change.number, existing.id, response.body) : await options.client.createNote(projectId, options.change.change.number, response.body);
|
|
10828
|
+
return {
|
|
10829
|
+
action: existing ? "updated" : "created",
|
|
10830
|
+
id: note.id
|
|
10831
|
+
};
|
|
10832
|
+
}
|
|
10833
|
+
async function loadGitLabPriorReviewState(options) {
|
|
10834
|
+
const state = extractPriorReviewState(await loadGitLabPriorMainComment(options), options.change.change.number);
|
|
10835
|
+
if (!state) return void 0;
|
|
10836
|
+
const owner = await options.client.currentUser();
|
|
10837
|
+
const bodies = discussionNotes(await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number)).filter((note) => note.author?.username === owner.username).map((note) => note.body);
|
|
10838
|
+
return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
|
|
10839
|
+
}
|
|
10840
|
+
async function loadGitLabPriorMainComment(options) {
|
|
10841
|
+
const owner = await options.client.currentUser();
|
|
10842
|
+
return ownedNote(await options.client.listNotes(gitLabCoordinates$1(options.change).projectId, options.change.change.number), owner.username, mainMarker(options.change.change.number))?.body;
|
|
10843
|
+
}
|
|
10844
|
+
async function loadGitLabInlineThreadContexts(options) {
|
|
10845
|
+
const owner = await options.client.currentUser();
|
|
10846
|
+
return (await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number)).flatMap((discussion) => {
|
|
10847
|
+
const root = discussion.notes[0];
|
|
10848
|
+
const marker = root ? extractInlineFindingMarkerRecords([root.body])[0] : void 0;
|
|
10849
|
+
if (!root || !marker || root.author?.username !== owner.username) return [];
|
|
10850
|
+
return [{
|
|
10851
|
+
findingId: marker.id,
|
|
10852
|
+
findingHeadSha: marker.head,
|
|
10853
|
+
parentCommentId: root.id,
|
|
10854
|
+
parentBody: root.body,
|
|
10855
|
+
threadId: discussion.id,
|
|
10856
|
+
threadResolved: root.resolved ?? false,
|
|
10857
|
+
comments: discussion.notes.map((note) => ({
|
|
10858
|
+
id: note.id,
|
|
10859
|
+
body: note.body,
|
|
10860
|
+
authorLogin: note.author?.username
|
|
10861
|
+
}))
|
|
10862
|
+
}];
|
|
10863
|
+
});
|
|
10864
|
+
}
|
|
10865
|
+
async function publishGitLabThreadActions(options) {
|
|
10866
|
+
if (options.actions.length === 0) return { errors: [] };
|
|
10867
|
+
const { projectId } = gitLabCoordinates$1(options.change);
|
|
10868
|
+
await assertCurrentHead(options.client, projectId, options.change, options.reviewedHeadSha);
|
|
10869
|
+
const discussions = options.discussions ?? await options.client.listDiscussions(projectId, options.change.change.number);
|
|
10870
|
+
const byNote = new Map(discussions.flatMap((discussion) => discussion.notes.map((note) => [note.id, discussion])));
|
|
10871
|
+
const errors = [];
|
|
10872
|
+
for (const action of options.actions) {
|
|
10873
|
+
const error = await publishGitLabThreadAction({
|
|
10874
|
+
client: options.client,
|
|
10875
|
+
projectId,
|
|
10876
|
+
changeNumber: options.change.change.number,
|
|
10877
|
+
action,
|
|
10878
|
+
discussion: action.threadId ? discussions.find((candidate) => candidate.id === action.threadId) : byNote.get(action.commentId)
|
|
10879
|
+
});
|
|
10880
|
+
if (error) errors.push(error);
|
|
10881
|
+
}
|
|
10882
|
+
return { errors };
|
|
10883
|
+
}
|
|
10884
|
+
async function publishGitLabThreadAction(options) {
|
|
10885
|
+
if (!options.discussion) return `GitLab discussion not found for comment ${options.action.commentId}`;
|
|
10886
|
+
try {
|
|
10887
|
+
if (!options.discussion.notes.some((note) => note.body.includes(options.action.responseKey))) await options.client.replyDiscussion(options.projectId, options.changeNumber, options.discussion.id, options.action.body);
|
|
10888
|
+
if (options.action.kind === "resolve" && !options.discussion.notes[0]?.resolved) await options.client.resolveDiscussion(options.projectId, options.changeNumber, options.discussion.id);
|
|
10889
|
+
} catch (error) {
|
|
10890
|
+
return error instanceof Error ? error.message : String(error);
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
function gitLabPosition(item, refs) {
|
|
10894
|
+
const oldPath = item.previousPath ?? item.path;
|
|
10895
|
+
const position = {
|
|
10896
|
+
position_type: "text",
|
|
10897
|
+
base_sha: refs.base_sha,
|
|
10898
|
+
start_sha: refs.start_sha,
|
|
10899
|
+
head_sha: refs.head_sha,
|
|
10900
|
+
old_path: oldPath,
|
|
10901
|
+
new_path: item.path,
|
|
10902
|
+
...item.side === "RIGHT" ? { new_line: item.endLine } : { old_line: item.endLine }
|
|
10903
|
+
};
|
|
10904
|
+
if (item.startLine !== item.endLine) {
|
|
10905
|
+
const type = item.side === "RIGHT" ? "new" : "old";
|
|
10906
|
+
const linePath = type === "old" ? oldPath : item.path;
|
|
10907
|
+
position.line_range = {
|
|
10908
|
+
start: lineRangePoint(linePath, type, item.startLine),
|
|
10909
|
+
end: lineRangePoint(linePath, type, item.endLine)
|
|
10910
|
+
};
|
|
10911
|
+
}
|
|
10912
|
+
return position;
|
|
10913
|
+
}
|
|
10914
|
+
function lineRangePoint(path, type, line) {
|
|
10915
|
+
return {
|
|
10916
|
+
line_code: `${new Bun.CryptoHasher("sha1").update(path).digest("hex")}_${type === "old" ? line : 0}_${type === "new" ? line : 0}`,
|
|
10917
|
+
type,
|
|
10918
|
+
...type === "old" ? { old_line: line } : { new_line: line }
|
|
10919
|
+
};
|
|
10920
|
+
}
|
|
10921
|
+
function gitLabInlineBody(item) {
|
|
10922
|
+
const offset = item.endLine - item.startLine;
|
|
10923
|
+
return item.body.replaceAll(/(`{3,})suggestion(\r?\n)/g, `$1suggestion:-${offset}+0$2`);
|
|
10924
|
+
}
|
|
10925
|
+
async function assertCurrentHead(client, projectId, change, reviewedHeadSha = change.change.head.sha) {
|
|
10926
|
+
const current = await client.getMergeRequest(projectId, change.change.number);
|
|
10927
|
+
if (current.diff_refs.head_sha !== reviewedHeadSha) throw new Error(`GitLab merge request head changed from ${reviewedHeadSha} to ${current.diff_refs.head_sha}`);
|
|
10928
|
+
return current;
|
|
10929
|
+
}
|
|
10930
|
+
function gitLabCoordinates$1(change) {
|
|
10931
|
+
if (change.coordinates?.provider !== "gitlab") throw new Error("GitLab adapter requires GitLab coordinates");
|
|
10932
|
+
return change.coordinates;
|
|
10933
|
+
}
|
|
10934
|
+
function ownedNote(notes, username, marker) {
|
|
10935
|
+
return notes.find((note) => note.author?.username === username && note.body.trimStart().startsWith(marker));
|
|
10936
|
+
}
|
|
10937
|
+
function discussionNotes(discussions) {
|
|
10938
|
+
return discussions.flatMap((discussion) => discussion.notes);
|
|
10939
|
+
}
|
|
10940
|
+
function mainMarker(changeNumber) {
|
|
10941
|
+
return `<!-- pipr:main-comment change=${changeNumber} `;
|
|
10942
|
+
}
|
|
10943
|
+
//#endregion
|
|
10944
|
+
//#region src/hosts/gitlab/workspace.ts
|
|
10945
|
+
function ensureGitLabHeadCheckout(options) {
|
|
10946
|
+
return ensureCodeHostHeadCheckout({
|
|
10947
|
+
rootDir: options.rootDir,
|
|
10948
|
+
headSha: options.change.change.head.sha,
|
|
10949
|
+
fetchRef: `refs/merge-requests/${options.change.change.number}/head`
|
|
10950
|
+
});
|
|
10951
|
+
}
|
|
10952
|
+
//#endregion
|
|
10953
|
+
//#region src/hosts/gitlab/adapter.ts
|
|
10954
|
+
function createGitLabHostAdapter(options = {}) {
|
|
10955
|
+
const client = options.client ?? createGitLabClient(options.env);
|
|
10956
|
+
return {
|
|
10957
|
+
id: "gitlab",
|
|
10958
|
+
capabilities: {
|
|
10959
|
+
commandComments: true,
|
|
10960
|
+
reviewCommentReplies: true,
|
|
10961
|
+
threadResolution: true,
|
|
10962
|
+
multilineInlineComments: true,
|
|
10963
|
+
suggestedChanges: true,
|
|
10964
|
+
statuses: true
|
|
10965
|
+
},
|
|
10966
|
+
events: {
|
|
10967
|
+
parseEvent(parseOptions) {
|
|
10968
|
+
return parseGitLabEvent({
|
|
10969
|
+
...parseOptions,
|
|
10970
|
+
loadChangeRequest: (ref) => client.loadChange(ref),
|
|
10971
|
+
resolveReplyParent: ({ projectId, changeNumber, noteId, discussionId }) => client.findReplyParent(projectId, changeNumber, noteId, discussionId)
|
|
8507
10972
|
});
|
|
10973
|
+
},
|
|
10974
|
+
loadChangeRequest(ref) {
|
|
10975
|
+
const coordinates = gitLabCoordinates(ref.repository.slug, ref.repository.url);
|
|
10976
|
+
return client.loadChange({
|
|
10977
|
+
projectId: coordinates.projectId,
|
|
10978
|
+
projectPath: coordinates.projectPath,
|
|
10979
|
+
changeNumber: ref.changeNumber
|
|
10980
|
+
}).then((loaded) => ({
|
|
10981
|
+
...loaded,
|
|
10982
|
+
eventName: ref.eventName,
|
|
10983
|
+
action: ref.action,
|
|
10984
|
+
rawAction: ref.rawAction,
|
|
10985
|
+
workspace: ref.workspace
|
|
10986
|
+
}));
|
|
10987
|
+
}
|
|
10988
|
+
},
|
|
10989
|
+
workspace: { ensureHeadCheckout: ensureGitLabHeadCheckout },
|
|
10990
|
+
permissions: { getRepositoryPermission({ change, actor }) {
|
|
10991
|
+
return client.getRepositoryPermission(gitLabCoordinates(change.repository.slug, change.repository.url).projectId, actor);
|
|
10992
|
+
} },
|
|
10993
|
+
publication: {
|
|
10994
|
+
publish: ({ plan, change }) => publishGitLabPlan({
|
|
10995
|
+
client,
|
|
10996
|
+
plan,
|
|
10997
|
+
change
|
|
10998
|
+
}),
|
|
10999
|
+
publishCommandResponse: (args) => publishGitLabCommandResponse({
|
|
11000
|
+
client,
|
|
11001
|
+
...args
|
|
11002
|
+
}),
|
|
11003
|
+
publishThreadActions: (args) => publishGitLabThreadActions({
|
|
11004
|
+
client,
|
|
11005
|
+
...args
|
|
11006
|
+
})
|
|
11007
|
+
},
|
|
11008
|
+
comments: {
|
|
11009
|
+
loadPriorReviewState: ({ change }) => loadGitLabPriorReviewState({
|
|
11010
|
+
client,
|
|
11011
|
+
change
|
|
11012
|
+
}),
|
|
11013
|
+
loadPriorMainComment: ({ change }) => loadGitLabPriorMainComment({
|
|
11014
|
+
client,
|
|
11015
|
+
change
|
|
11016
|
+
}),
|
|
11017
|
+
loadInlineThreadContexts: ({ change }) => loadGitLabInlineThreadContexts({
|
|
11018
|
+
client,
|
|
11019
|
+
change
|
|
11020
|
+
})
|
|
11021
|
+
},
|
|
11022
|
+
statuses: {
|
|
11023
|
+
isAvailable: () => true,
|
|
11024
|
+
async upsert({ change, name, state, summary, status }) {
|
|
11025
|
+
const id = await client.setStatus(gitLabChangeCoordinates(change).projectId, change.change.head.sha, name, state, summary);
|
|
11026
|
+
return status ?? {
|
|
11027
|
+
id,
|
|
11028
|
+
name
|
|
11029
|
+
};
|
|
8508
11030
|
}
|
|
8509
11031
|
}
|
|
8510
11032
|
};
|
|
8511
11033
|
}
|
|
11034
|
+
function gitLabChangeCoordinates(change) {
|
|
11035
|
+
if (change.coordinates?.provider !== "gitlab") throw new Error("GitLab adapter requires GitLab coordinates");
|
|
11036
|
+
return change.coordinates;
|
|
11037
|
+
}
|
|
11038
|
+
function gitLabCoordinates(slug, url) {
|
|
11039
|
+
return {
|
|
11040
|
+
projectId: url?.match(/\/projects\/(\d+)/)?.[1] ?? slug,
|
|
11041
|
+
projectPath: slug
|
|
11042
|
+
};
|
|
11043
|
+
}
|
|
11044
|
+
//#endregion
|
|
11045
|
+
//#region src/hosts/selection.ts
|
|
11046
|
+
const codeHostIds = [
|
|
11047
|
+
"github",
|
|
11048
|
+
"gitlab",
|
|
11049
|
+
"azure-devops",
|
|
11050
|
+
"bitbucket"
|
|
11051
|
+
];
|
|
11052
|
+
function resolveCodeHostId(options) {
|
|
11053
|
+
const selected = options.explicitHost ?? options.env.PIPR_CODE_HOST;
|
|
11054
|
+
if (selected) return parseCodeHostId(selected);
|
|
11055
|
+
const detected = [];
|
|
11056
|
+
if (options.env.GITHUB_ACTIONS !== void 0) detected.push("github");
|
|
11057
|
+
if (options.env.GITLAB_CI !== void 0) detected.push("gitlab");
|
|
11058
|
+
if (options.env.TF_BUILD !== void 0) detected.push("azure-devops");
|
|
11059
|
+
if (options.env.BITBUCKET_BUILD_NUMBER !== void 0) detected.push("bitbucket");
|
|
11060
|
+
if (detected.length === 1) {
|
|
11061
|
+
const host = detected[0];
|
|
11062
|
+
if (host) return host;
|
|
11063
|
+
}
|
|
11064
|
+
if (detected.length > 1) throw new Error(`Multiple code hosts detected: ${detected.join(", ")}`);
|
|
11065
|
+
throw new Error("A code host must be selected");
|
|
11066
|
+
}
|
|
11067
|
+
function parseCodeHostId(value) {
|
|
11068
|
+
switch (value) {
|
|
11069
|
+
case "github":
|
|
11070
|
+
case "gitlab":
|
|
11071
|
+
case "azure-devops":
|
|
11072
|
+
case "bitbucket": return value;
|
|
11073
|
+
default: throw new Error(`Unsupported code host '${value}'. Supported hosts: ${codeHostIds.join(", ")}`);
|
|
11074
|
+
}
|
|
11075
|
+
}
|
|
8512
11076
|
//#endregion
|
|
8513
|
-
//#region src/
|
|
8514
|
-
function
|
|
11077
|
+
//#region src/host-run/adapter.ts
|
|
11078
|
+
function assertTrustedHostRunProviderEnv(options, trustedConfig) {
|
|
8515
11079
|
const env = options.env ?? process.env;
|
|
8516
11080
|
const missing = [];
|
|
8517
11081
|
for (const provider of trustedConfig.providers) if (!env[provider.apiKeyEnv]) missing.push(provider.apiKeyEnv);
|
|
8518
11082
|
if (missing.length > 0) throw new Error(`Missing provider env vars: ${missing.join(", ")}`);
|
|
8519
11083
|
}
|
|
8520
|
-
function
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
11084
|
+
function createHostRunAdapter(options) {
|
|
11085
|
+
if (options.hostAdapter) return options.hostAdapter;
|
|
11086
|
+
const host = resolveCodeHostId({
|
|
11087
|
+
explicitHost: options.host,
|
|
11088
|
+
env: options.env ?? process.env
|
|
8525
11089
|
});
|
|
11090
|
+
if (host !== "github") {
|
|
11091
|
+
if (host === "azure-devops") return createAzureDevOpsHostAdapter({ env: options.env });
|
|
11092
|
+
if (host === "gitlab") return createGitLabHostAdapter({ env: options.env });
|
|
11093
|
+
if (host === "bitbucket") return createBitbucketHostAdapter({ env: options.env });
|
|
11094
|
+
throw new Error(`Code host adapter '${host}' is not available in this build`);
|
|
11095
|
+
}
|
|
11096
|
+
return createGitHubHostAdapter({ env: options.env });
|
|
8526
11097
|
}
|
|
8527
11098
|
//#endregion
|
|
8528
|
-
//#region src/
|
|
11099
|
+
//#region src/host-run/logging.ts
|
|
8529
11100
|
async function logPhase(log, name, run) {
|
|
8530
11101
|
const started = Date.now();
|
|
8531
11102
|
log.info(`${name} start`);
|
|
@@ -8574,8 +11145,8 @@ function addProviderSecrets(log, config, env) {
|
|
|
8574
11145
|
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
8575
11146
|
}
|
|
8576
11147
|
//#endregion
|
|
8577
|
-
//#region src/
|
|
8578
|
-
const genericCheckFailureSummary = "pipr failed; see
|
|
11148
|
+
//#region src/host-run/runtime-checks.ts
|
|
11149
|
+
const genericCheckFailureSummary = "pipr failed; see runner logs for details.";
|
|
8579
11150
|
async function startRuntimeChecks(options) {
|
|
8580
11151
|
if (!canStartRuntimeChecks(options)) return;
|
|
8581
11152
|
const tasks = options.selectedTasks;
|
|
@@ -8598,8 +11169,8 @@ async function startRuntimeChecks(options) {
|
|
|
8598
11169
|
try {
|
|
8599
11170
|
await startTaskCheckRuns(started);
|
|
8600
11171
|
if (aggregateName) {
|
|
8601
|
-
started.aggregate = await
|
|
8602
|
-
started.log?.info("
|
|
11172
|
+
started.aggregate = await createStatusOrThrow(started, aggregateName, "pipr review is running.");
|
|
11173
|
+
started.log?.info("status created", {
|
|
8603
11174
|
name: aggregateName,
|
|
8604
11175
|
kind: "aggregate"
|
|
8605
11176
|
});
|
|
@@ -8611,7 +11182,7 @@ async function startRuntimeChecks(options) {
|
|
|
8611
11182
|
return started;
|
|
8612
11183
|
}
|
|
8613
11184
|
async function finalizeRuntimeChecks(checks, options) {
|
|
8614
|
-
if (!checks?.adapter.
|
|
11185
|
+
if (!checks?.adapter.statuses) return;
|
|
8615
11186
|
const taskResults = checks.tasks.map((task) => taskCheckResultForFinalization(task, checks.outcomes.get(task.name), options));
|
|
8616
11187
|
const taskError = await updateTaskCheckRuns(checks, taskResults);
|
|
8617
11188
|
const aggregateError = await updateAggregateCheckRun(checks, taskResults, options);
|
|
@@ -8619,37 +11190,38 @@ async function finalizeRuntimeChecks(checks, options) {
|
|
|
8619
11190
|
if (firstError !== void 0) throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
8620
11191
|
}
|
|
8621
11192
|
function canStartRuntimeChecks(options) {
|
|
8622
|
-
return options.
|
|
11193
|
+
return options.taskName === void 0 && options.adapter.capabilities.statuses && Boolean(options.adapter.statuses?.isAvailable(options.event));
|
|
8623
11194
|
}
|
|
8624
11195
|
async function startTaskCheckRuns(started) {
|
|
8625
11196
|
for (const task of started.tasks) {
|
|
8626
11197
|
const settings = taskCheckSettings(task);
|
|
8627
11198
|
if (!settings.individual) continue;
|
|
8628
|
-
const run = await
|
|
11199
|
+
const run = await createStatusOrThrow(started, settings.name, "pipr task is running.");
|
|
8629
11200
|
started.taskRuns.set(task.name, run);
|
|
8630
|
-
started.log?.info("
|
|
11201
|
+
started.log?.info("status created", {
|
|
8631
11202
|
name: settings.name,
|
|
8632
11203
|
kind: "task",
|
|
8633
11204
|
task: task.name
|
|
8634
11205
|
});
|
|
8635
11206
|
}
|
|
8636
11207
|
}
|
|
8637
|
-
async function
|
|
11208
|
+
async function createStatusOrThrow(checks, name, summary) {
|
|
8638
11209
|
try {
|
|
8639
|
-
const
|
|
8640
|
-
if (!
|
|
8641
|
-
return await
|
|
11210
|
+
const statuses = checks.adapter.statuses;
|
|
11211
|
+
if (!statuses) throw new Error("status publication is not available");
|
|
11212
|
+
return await statuses.upsert({
|
|
8642
11213
|
change: checks.event,
|
|
8643
11214
|
name,
|
|
11215
|
+
state: "pending",
|
|
8644
11216
|
summary
|
|
8645
11217
|
});
|
|
8646
11218
|
} catch (error) {
|
|
8647
|
-
throw
|
|
11219
|
+
throw statusPermissionError(error);
|
|
8648
11220
|
}
|
|
8649
11221
|
}
|
|
8650
|
-
function
|
|
11222
|
+
function statusPermissionError(error) {
|
|
8651
11223
|
const message = error instanceof Error ? error.message : String(error);
|
|
8652
|
-
return /* @__PURE__ */ new Error(`Unable to
|
|
11224
|
+
return /* @__PURE__ */ new Error(`Unable to publish code host status. Check the adapter credential scopes. ${message}`);
|
|
8653
11225
|
}
|
|
8654
11226
|
async function updateTaskCheckRuns(checks, taskResults) {
|
|
8655
11227
|
let firstError;
|
|
@@ -8667,20 +11239,21 @@ async function updateAggregateCheckRun(checks, taskResults, options) {
|
|
|
8667
11239
|
}
|
|
8668
11240
|
async function updateCheckRunOrError(checks, checkRun, result) {
|
|
8669
11241
|
try {
|
|
8670
|
-
await checks.adapter.
|
|
11242
|
+
await checks.adapter.statuses?.upsert({
|
|
8671
11243
|
change: checks.event,
|
|
8672
|
-
checkRun,
|
|
8673
|
-
|
|
11244
|
+
name: checkRun.name,
|
|
11245
|
+
status: checkRun,
|
|
11246
|
+
state: result.conclusion,
|
|
8674
11247
|
summary: result.summary
|
|
8675
11248
|
});
|
|
8676
|
-
checks.log?.info("
|
|
11249
|
+
checks.log?.info("status finalized", {
|
|
8677
11250
|
name: checkRun.name,
|
|
8678
11251
|
conclusion: result.conclusion,
|
|
8679
11252
|
summary: result.summary
|
|
8680
11253
|
});
|
|
8681
11254
|
return;
|
|
8682
11255
|
} catch (error) {
|
|
8683
|
-
checks.log?.warning("
|
|
11256
|
+
checks.log?.warning("status finalize failed", {
|
|
8684
11257
|
name: checkRun.name,
|
|
8685
11258
|
error: error instanceof Error ? error.message : String(error)
|
|
8686
11259
|
});
|
|
@@ -8747,7 +11320,7 @@ function taskCheckSettings(task) {
|
|
|
8747
11320
|
};
|
|
8748
11321
|
}
|
|
8749
11322
|
//#endregion
|
|
8750
|
-
//#region src/
|
|
11323
|
+
//#region src/host-run/review-publishing.ts
|
|
8751
11324
|
async function runTrustedReviewAndPublish(options) {
|
|
8752
11325
|
const checks = await startRuntimeChecks({
|
|
8753
11326
|
adapter: options.adapter,
|
|
@@ -8798,13 +11371,14 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
8798
11371
|
const publish = options.adapter.publication?.publish;
|
|
8799
11372
|
if (!publish) throw new Error("review publication is not available for this code host");
|
|
8800
11373
|
const publication = await options.log.group("publish review", async () => {
|
|
11374
|
+
const publicationPlan = publicationPlanForHostCapabilities(review.publicationPlan, options.adapter.capabilities);
|
|
8801
11375
|
options.log.info("publication plan", {
|
|
8802
|
-
inlineItems:
|
|
8803
|
-
threadActions:
|
|
11376
|
+
inlineItems: publicationPlan.inlineItems.length,
|
|
11377
|
+
threadActions: publicationPlan.threadActions.length
|
|
8804
11378
|
});
|
|
8805
11379
|
const result = await publish({
|
|
8806
11380
|
change: options.event,
|
|
8807
|
-
plan:
|
|
11381
|
+
plan: publicationPlan
|
|
8808
11382
|
});
|
|
8809
11383
|
options.log.notice("publication result", {
|
|
8810
11384
|
main: result.mainComment.action,
|
|
@@ -8832,7 +11406,7 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
8832
11406
|
}
|
|
8833
11407
|
}
|
|
8834
11408
|
//#endregion
|
|
8835
|
-
//#region src/
|
|
11409
|
+
//#region src/host-run/git-project.ts
|
|
8836
11410
|
async function loadRuntimeProjectFromGitCommit(options) {
|
|
8837
11411
|
const configDir = resolveContainedConfigDir(options);
|
|
8838
11412
|
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
@@ -8911,7 +11485,7 @@ function showFileAtCommit(rootDir, commitSha, filePath) {
|
|
|
8911
11485
|
return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
|
|
8912
11486
|
}
|
|
8913
11487
|
//#endregion
|
|
8914
|
-
//#region src/
|
|
11488
|
+
//#region src/host-run/trusted-runtime.ts
|
|
8915
11489
|
async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
8916
11490
|
const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
|
|
8917
11491
|
rootDir: options.rootDir,
|
|
@@ -8924,30 +11498,78 @@ async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
|
8924
11498
|
}
|
|
8925
11499
|
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
8926
11500
|
addProviderSecrets(log, config, options.env);
|
|
8927
|
-
|
|
11501
|
+
assertTrustedHostRunProviderEnv(options, config);
|
|
8928
11502
|
await logPhase(log, "checkout head", async () => {
|
|
8929
|
-
adapter.workspace.ensureHeadCheckout({
|
|
11503
|
+
await adapter.workspace.ensureHeadCheckout({
|
|
8930
11504
|
rootDir: options.rootDir,
|
|
8931
11505
|
change: event
|
|
8932
11506
|
});
|
|
8933
11507
|
});
|
|
8934
11508
|
}
|
|
8935
11509
|
//#endregion
|
|
8936
|
-
//#region src/
|
|
8937
|
-
async function
|
|
8938
|
-
|
|
11510
|
+
//#region src/host-run/change-request-entry.ts
|
|
11511
|
+
async function runChangeRequestHostRunCommand(options, adapter, log, event) {
|
|
11512
|
+
logEventContext(log, event);
|
|
11513
|
+
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
11514
|
+
if (options.dryRun) {
|
|
11515
|
+
log.notice("dry run stop before review runtime, model, or GitHub publishing calls");
|
|
11516
|
+
return {
|
|
11517
|
+
kind: "dry-run",
|
|
11518
|
+
event,
|
|
11519
|
+
configSource: trustedRuntime.settings.source
|
|
11520
|
+
};
|
|
11521
|
+
}
|
|
11522
|
+
await prepareTrustedHeadCheckout(options, adapter, trustedRuntime.settings.config, event, log);
|
|
11523
|
+
const dispatch = dispatchRuntimeEntry({
|
|
11524
|
+
kind: "change-request",
|
|
11525
|
+
plan: trustedRuntime.plan,
|
|
11526
|
+
event
|
|
11527
|
+
});
|
|
11528
|
+
const selectedTasks = dispatch.kind === "change-request" ? dispatch.tasks : [];
|
|
11529
|
+
log.notice("dispatch", { selectedTasks: selectedTasks.map((task) => task.name) });
|
|
11530
|
+
const completed = await runTrustedReviewAndPublish({
|
|
11531
|
+
options,
|
|
11532
|
+
adapter,
|
|
11533
|
+
trustedRuntime,
|
|
11534
|
+
event,
|
|
11535
|
+
selectedTasks,
|
|
11536
|
+
log
|
|
11537
|
+
});
|
|
11538
|
+
if (completed.kind === "skipped") {
|
|
11539
|
+
log.notice("event ignored", { reason: completed.reason });
|
|
11540
|
+
return {
|
|
11541
|
+
kind: "ignored",
|
|
11542
|
+
reason: completed.reason
|
|
11543
|
+
};
|
|
11544
|
+
}
|
|
11545
|
+
if (completed.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
11546
|
+
return {
|
|
11547
|
+
kind: "review",
|
|
11548
|
+
event,
|
|
11549
|
+
configSource: trustedRuntime.settings.source,
|
|
11550
|
+
review: completed.review,
|
|
11551
|
+
publication: completed.publication
|
|
11552
|
+
};
|
|
11553
|
+
}
|
|
11554
|
+
//#endregion
|
|
11555
|
+
//#region src/host-run/command-entry.ts
|
|
11556
|
+
async function runIssueCommentHostRunCommand(options, adapter, log, comment) {
|
|
11557
|
+
if (!adapter.capabilities.commandComments) {
|
|
11558
|
+
const ignored = {
|
|
11559
|
+
kind: "ignored",
|
|
11560
|
+
reason: "host adapter does not support commands"
|
|
11561
|
+
};
|
|
11562
|
+
log.notice("event ignored", { reason: ignored.reason });
|
|
11563
|
+
return ignored;
|
|
11564
|
+
}
|
|
11565
|
+
const prepared = await prepareIssueCommentCommand(options, adapter, log, comment);
|
|
8939
11566
|
if (prepared.kind === "ignored") {
|
|
8940
|
-
log.notice("
|
|
11567
|
+
log.notice("event ignored", { reason: prepared.reason });
|
|
8941
11568
|
return prepared;
|
|
8942
11569
|
}
|
|
8943
11570
|
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
8944
11571
|
}
|
|
8945
|
-
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
8946
|
-
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
8947
|
-
eventPath: options.eventPath,
|
|
8948
|
-
env: options.env ?? process.env,
|
|
8949
|
-
workspace: options.rootDir
|
|
8950
|
-
}));
|
|
11572
|
+
async function prepareIssueCommentCommand(options, adapter, log, comment) {
|
|
8951
11573
|
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
8952
11574
|
if (runnable.kind === "ignored") return runnable;
|
|
8953
11575
|
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
@@ -8964,6 +11586,7 @@ async function prepareIssueCommentCommand(options, adapter, log) {
|
|
|
8964
11586
|
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
8965
11587
|
platform: { id: adapter.id },
|
|
8966
11588
|
repository: loaded.repository,
|
|
11589
|
+
coordinates: loaded.coordinates,
|
|
8967
11590
|
change: loaded.change,
|
|
8968
11591
|
workspace: loaded.workspace ?? comment.workspace
|
|
8969
11592
|
});
|
|
@@ -9008,7 +11631,7 @@ function runnableIssueCommentCommand(comment, dryRun) {
|
|
|
9008
11631
|
async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
|
|
9009
11632
|
const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
|
|
9010
11633
|
const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
|
|
9011
|
-
|
|
11634
|
+
change: prepared.event,
|
|
9012
11635
|
actor: prepared.comment.actor
|
|
9013
11636
|
}));
|
|
9014
11637
|
log.notice("command dispatch", {
|
|
@@ -9078,7 +11701,7 @@ async function issueCommentCommandResult(options) {
|
|
|
9078
11701
|
kind: "ignored",
|
|
9079
11702
|
reason: options.completed.reason
|
|
9080
11703
|
};
|
|
9081
|
-
if (options.completed.kind === "command-response") return await
|
|
11704
|
+
if (options.completed.kind === "command-response") return await publishCommandResponseHostRunResult({
|
|
9082
11705
|
adapter: options.adapter,
|
|
9083
11706
|
completed: options.completed,
|
|
9084
11707
|
event: options.event,
|
|
@@ -9094,7 +11717,7 @@ async function issueCommentCommandResult(options) {
|
|
|
9094
11717
|
publication: options.completed.publication
|
|
9095
11718
|
};
|
|
9096
11719
|
}
|
|
9097
|
-
async function
|
|
11720
|
+
async function publishCommandResponseHostRunResult(options) {
|
|
9098
11721
|
const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
|
|
9099
11722
|
if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
|
|
9100
11723
|
const publication = await publishCommandResponse({
|
|
@@ -9113,76 +11736,21 @@ async function publishCommandResponseActionResult(options) {
|
|
|
9113
11736
|
};
|
|
9114
11737
|
}
|
|
9115
11738
|
//#endregion
|
|
9116
|
-
//#region src/
|
|
9117
|
-
async function
|
|
9118
|
-
const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
|
|
9119
|
-
eventPath: options.eventPath,
|
|
9120
|
-
env: options.env ?? process.env,
|
|
9121
|
-
workspace: options.rootDir
|
|
9122
|
-
}));
|
|
9123
|
-
logEventContext(log, event);
|
|
9124
|
-
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
9125
|
-
if (options.dryRun) {
|
|
9126
|
-
log.notice("dry run stop before review runtime, model, or GitHub publishing calls");
|
|
9127
|
-
return {
|
|
9128
|
-
kind: "dry-run",
|
|
9129
|
-
event,
|
|
9130
|
-
configSource: trustedRuntime.settings.source
|
|
9131
|
-
};
|
|
9132
|
-
}
|
|
9133
|
-
await prepareTrustedHeadCheckout(options, adapter, trustedRuntime.settings.config, event, log);
|
|
9134
|
-
const dispatch = dispatchRuntimeEntry({
|
|
9135
|
-
kind: "change-request",
|
|
9136
|
-
plan: trustedRuntime.plan,
|
|
9137
|
-
event
|
|
9138
|
-
});
|
|
9139
|
-
const selectedTasks = dispatch.kind === "change-request" ? dispatch.tasks : [];
|
|
9140
|
-
log.notice("dispatch", { selectedTasks: selectedTasks.map((task) => task.name) });
|
|
9141
|
-
const completed = await runTrustedReviewAndPublish({
|
|
9142
|
-
options,
|
|
9143
|
-
adapter,
|
|
9144
|
-
trustedRuntime,
|
|
9145
|
-
event,
|
|
9146
|
-
selectedTasks,
|
|
9147
|
-
log
|
|
9148
|
-
});
|
|
9149
|
-
if (completed.kind === "skipped") {
|
|
9150
|
-
log.notice("action ignored", { reason: completed.reason });
|
|
9151
|
-
return {
|
|
9152
|
-
kind: "ignored",
|
|
9153
|
-
reason: completed.reason
|
|
9154
|
-
};
|
|
9155
|
-
}
|
|
9156
|
-
if (completed.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
9157
|
-
return {
|
|
9158
|
-
kind: "review",
|
|
9159
|
-
event,
|
|
9160
|
-
configSource: trustedRuntime.settings.source,
|
|
9161
|
-
review: completed.review,
|
|
9162
|
-
publication: completed.publication
|
|
9163
|
-
};
|
|
9164
|
-
}
|
|
9165
|
-
//#endregion
|
|
9166
|
-
//#region src/action/verifier-entry.ts
|
|
9167
|
-
async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
11739
|
+
//#region src/host-run/verifier-entry.ts
|
|
11740
|
+
async function runReviewCommentReplyHostRunCommand(options, adapter, log, reply) {
|
|
9168
11741
|
const capabilities = reviewCommentReplyDispatchCapabilities(options, adapter);
|
|
9169
11742
|
if (capabilities.kind === "ignored") {
|
|
9170
|
-
log.notice("
|
|
11743
|
+
log.notice("event ignored", { reason: capabilities.reason });
|
|
9171
11744
|
return capabilities;
|
|
9172
11745
|
}
|
|
9173
|
-
const reply = await logPhase(log, "parse review comment reply", async () => capabilities.resolveReviewCommentReply({
|
|
9174
|
-
eventPath: options.eventPath,
|
|
9175
|
-
env: options.env ?? process.env,
|
|
9176
|
-
workspace: options.rootDir
|
|
9177
|
-
}));
|
|
9178
11746
|
const runnable = runnableReviewCommentReply(reply);
|
|
9179
11747
|
if (runnable.kind === "ignored") {
|
|
9180
|
-
log.notice("
|
|
11748
|
+
log.notice("event ignored", { reason: runnable.reason });
|
|
9181
11749
|
return runnable;
|
|
9182
11750
|
}
|
|
9183
11751
|
const prepared = await prepareReviewCommentVerifier(options, adapter, reply, log);
|
|
9184
11752
|
if (prepared.kind === "ignored") {
|
|
9185
|
-
log.notice("
|
|
11753
|
+
log.notice("event ignored", { reason: prepared.reason });
|
|
9186
11754
|
return prepared;
|
|
9187
11755
|
}
|
|
9188
11756
|
const result = await runReviewCommentVerifier(options, adapter, prepared, log);
|
|
@@ -9203,9 +11771,9 @@ async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
|
9203
11771
|
};
|
|
9204
11772
|
}
|
|
9205
11773
|
function reviewCommentReplyDispatchCapabilities(options, adapter) {
|
|
9206
|
-
if (!adapter.
|
|
11774
|
+
if (!adapter.capabilities.reviewCommentReplies || !adapter.capabilities.threadResolution) return {
|
|
9207
11775
|
kind: "ignored",
|
|
9208
|
-
reason: "host adapter does not support
|
|
11776
|
+
reason: "host adapter does not support verifier replies"
|
|
9209
11777
|
};
|
|
9210
11778
|
if (!adapter.publication?.publishThreadActions) return {
|
|
9211
11779
|
kind: "ignored",
|
|
@@ -9217,7 +11785,6 @@ function reviewCommentReplyDispatchCapabilities(options, adapter) {
|
|
|
9217
11785
|
};
|
|
9218
11786
|
return {
|
|
9219
11787
|
kind: "ready",
|
|
9220
|
-
resolveReviewCommentReply: adapter.events.resolveReviewCommentReply,
|
|
9221
11788
|
publishThreadActions: adapter.publication.publishThreadActions
|
|
9222
11789
|
};
|
|
9223
11790
|
}
|
|
@@ -9240,6 +11807,7 @@ async function prepareReviewCommentVerifier(options, adapter, reply, log) {
|
|
|
9240
11807
|
rawAction: loaded.rawAction ?? reply.rawAction,
|
|
9241
11808
|
platform: { id: adapter.id },
|
|
9242
11809
|
repository: loaded.repository,
|
|
11810
|
+
coordinates: loaded.coordinates,
|
|
9243
11811
|
change: loaded.change,
|
|
9244
11812
|
workspace: loaded.workspace ?? reply.workspace
|
|
9245
11813
|
});
|
|
@@ -9345,12 +11913,12 @@ async function verifierActorAllowed(adapter, event, reply, config) {
|
|
|
9345
11913
|
if (allowed === "any") return true;
|
|
9346
11914
|
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
9347
11915
|
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
9348
|
-
|
|
11916
|
+
change: event,
|
|
9349
11917
|
actor: reply.actor
|
|
9350
11918
|
}), "write");
|
|
9351
11919
|
}
|
|
9352
11920
|
//#endregion
|
|
9353
|
-
//#region src/
|
|
11921
|
+
//#region src/host-run/commands.ts
|
|
9354
11922
|
/** Initializes the official minimal `.pipr` project files. */
|
|
9355
11923
|
async function runInitCommand(options) {
|
|
9356
11924
|
return await initOfficialMinimalProject({
|
|
@@ -9380,21 +11948,19 @@ async function runInspectCommand(options) {
|
|
|
9380
11948
|
warnings: runtime.settings.warnings
|
|
9381
11949
|
};
|
|
9382
11950
|
}
|
|
9383
|
-
/** Loads the runtime config and
|
|
11951
|
+
/** Loads the runtime config and change request event without running review publication. */
|
|
9384
11952
|
async function runDryRunCommand(options) {
|
|
9385
11953
|
const runtime = await loadRuntimeProject({
|
|
9386
11954
|
...options,
|
|
9387
11955
|
requireProviderEnv: false
|
|
9388
11956
|
});
|
|
9389
|
-
const
|
|
11957
|
+
const hostEvent = await createHostRunAdapter(options).events.parseEvent({
|
|
9390
11958
|
eventPath: options.eventPath,
|
|
9391
|
-
env:
|
|
9392
|
-
...options.env,
|
|
9393
|
-
GITHUB_WORKSPACE: options.rootDir,
|
|
9394
|
-
GITHUB_EVENT_NAME: "pull_request"
|
|
9395
|
-
},
|
|
11959
|
+
env: options.env ?? process.env,
|
|
9396
11960
|
workspace: options.rootDir
|
|
9397
11961
|
});
|
|
11962
|
+
if (hostEvent.kind !== "change-request") throw new Error(`dry-run requires a change-request event, received ${hostEvent.kind}`);
|
|
11963
|
+
const event = hostEvent.change;
|
|
9398
11964
|
return {
|
|
9399
11965
|
configSource: runtime.settings.source,
|
|
9400
11966
|
event,
|
|
@@ -9403,7 +11969,7 @@ async function runDryRunCommand(options) {
|
|
|
9403
11969
|
}
|
|
9404
11970
|
/** Runs configured change-request tasks against local Git base and head revisions. */
|
|
9405
11971
|
async function runLocalReviewCommand(options) {
|
|
9406
|
-
const log = options.logSink ?
|
|
11972
|
+
const log = options.logSink ? createRuntimeLog({
|
|
9407
11973
|
logSink: options.logSink,
|
|
9408
11974
|
env: options.env
|
|
9409
11975
|
}) : void 0;
|
|
@@ -9467,36 +12033,43 @@ async function runLocalReviewCommand(options) {
|
|
|
9467
12033
|
});
|
|
9468
12034
|
return result;
|
|
9469
12035
|
}
|
|
9470
|
-
/** Runs
|
|
9471
|
-
async function
|
|
9472
|
-
return await
|
|
12036
|
+
/** Runs a normalized code host event through the selected adapter. */
|
|
12037
|
+
async function runHostRunCommand(options) {
|
|
12038
|
+
return await runHostRunCommandWithDependencies(options);
|
|
9473
12039
|
}
|
|
9474
|
-
async function
|
|
9475
|
-
const log =
|
|
12040
|
+
async function runHostRunCommandWithDependencies(options) {
|
|
12041
|
+
const log = createRuntimeLog({
|
|
9476
12042
|
logSink: options.logSink,
|
|
9477
12043
|
env: options.env
|
|
9478
12044
|
});
|
|
9479
|
-
return await log.group("pipr
|
|
9480
|
-
|
|
9481
|
-
log.notice("action start", {
|
|
9482
|
-
eventName,
|
|
12045
|
+
return await log.group("pipr host run", async () => {
|
|
12046
|
+
log.notice("host run start", {
|
|
9483
12047
|
dryRun: options.dryRun,
|
|
9484
12048
|
root: options.rootDir,
|
|
9485
12049
|
configDir: options.configDir
|
|
9486
12050
|
});
|
|
9487
|
-
const adapter =
|
|
12051
|
+
const adapter = createHostRunAdapter(options);
|
|
9488
12052
|
await logPhase(log, "workspace", async () => {
|
|
9489
12053
|
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
9490
12054
|
rootDir: options.rootDir,
|
|
9491
12055
|
env: options.env
|
|
9492
12056
|
});
|
|
9493
12057
|
});
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
12058
|
+
const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
|
|
12059
|
+
eventPath: options.eventPath,
|
|
12060
|
+
env: options.env ?? process.env,
|
|
12061
|
+
workspace: options.rootDir
|
|
12062
|
+
}));
|
|
12063
|
+
log.notice("event dispatch", { kind: event.kind });
|
|
12064
|
+
switch (event.kind) {
|
|
12065
|
+
case "ignored": return event;
|
|
12066
|
+
case "command-comment": return await runIssueCommentHostRunCommand(options, adapter, log, event.comment);
|
|
12067
|
+
case "review-comment-reply": return await runReviewCommentReplyHostRunCommand(options, adapter, log, event.reply);
|
|
12068
|
+
case "change-request": return await runChangeRequestHostRunCommand(options, adapter, log, event.change);
|
|
12069
|
+
}
|
|
9497
12070
|
});
|
|
9498
12071
|
}
|
|
9499
12072
|
//#endregion
|
|
9500
|
-
export { runInspectCommand as a,
|
|
12073
|
+
export { supportedOfficialInitRecipes as _, runInspectCommand as a, piRequiredCliFlags as b, createGitHubHostAdapter as c, createBitbucketClient as d, createAzureDevOpsClient as f, listOfficialInitRecipes as g, supportedOfficialInitAdapters as h, runInitCommand as i, PublicationError as l, redactPotentialSecrets as m, runHostRunCommand as n, runLocalReviewCommand as o, azureOrganizationFromUrl as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createGitLabClient as u, piBuiltinToolNames as v, piThinkingLevels as x, piReadOnlyToolNames as y };
|
|
9501
12074
|
|
|
9502
|
-
//# sourceMappingURL=commands-
|
|
12075
|
+
//# sourceMappingURL=commands-ZZkXwqEn.mjs.map
|