@usepipr/runtime 0.3.7 → 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-Bcta0T4L.d.mts → commands-YFjB3wx8.d.mts} +147 -214
- package/dist/commands-YFjB3wx8.d.mts.map +1 -0
- package/dist/{commands-DM63Wmy0.mjs → commands-ZZkXwqEn.mjs} +3364 -721
- 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-Bcta0T4L.d.mts.map +0 -1
- package/dist/commands-DM63Wmy0.mjs.map +0 -1
|
@@ -272,7 +272,10 @@ const piprConfigSchema = z.strictObject({
|
|
|
272
272
|
providers: z.array(providerConfigSchema).min(1),
|
|
273
273
|
publication: z.strictObject({
|
|
274
274
|
maxInlineComments: z.number().int().min(0).max(50).optional(),
|
|
275
|
-
autoResolve: autoResolveConfigSchema
|
|
275
|
+
autoResolve: autoResolveConfigSchema,
|
|
276
|
+
showHeader: z.boolean().default(true),
|
|
277
|
+
showFooter: z.boolean().default(true),
|
|
278
|
+
showStats: z.boolean().default(true)
|
|
276
279
|
}),
|
|
277
280
|
limits: z.strictObject({
|
|
278
281
|
timeoutSeconds: z.number().int().positive().max(3600).optional(),
|
|
@@ -292,6 +295,31 @@ const repositoryRefSchema = z.strictObject({
|
|
|
292
295
|
slug: nonEmptyStringSchema,
|
|
293
296
|
url: nonEmptyStringSchema.optional()
|
|
294
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
|
+
]);
|
|
295
323
|
const changeEndpointSchema = z.strictObject({
|
|
296
324
|
sha: nonEmptyStringSchema,
|
|
297
325
|
ref: nonEmptyStringSchema.optional(),
|
|
@@ -307,7 +335,8 @@ const changeRequestRefSchema = z.strictObject({
|
|
|
307
335
|
author: z.strictObject({ login: nonEmptyStringSchema }).optional(),
|
|
308
336
|
base: changeEndpointSchema,
|
|
309
337
|
head: changeEndpointSchema,
|
|
310
|
-
isFork: z.boolean().optional()
|
|
338
|
+
isFork: z.boolean().optional(),
|
|
339
|
+
isDraft: z.boolean().optional()
|
|
311
340
|
});
|
|
312
341
|
const changeRequestEventContextSchema = z.strictObject({
|
|
313
342
|
eventName: nonEmptyStringSchema,
|
|
@@ -315,6 +344,7 @@ const changeRequestEventContextSchema = z.strictObject({
|
|
|
315
344
|
rawAction: nonEmptyStringSchema.optional(),
|
|
316
345
|
platform: platformInfoSchema,
|
|
317
346
|
repository: repositoryRefSchema,
|
|
347
|
+
coordinates: codeHostCoordinatesSchema.optional(),
|
|
318
348
|
change: changeRequestRefSchema,
|
|
319
349
|
workspace: nonEmptyStringSchema
|
|
320
350
|
});
|
|
@@ -526,7 +556,7 @@ function compareStableSemver(left, right) {
|
|
|
526
556
|
}
|
|
527
557
|
//#endregion
|
|
528
558
|
//#region src/shared/version.ts
|
|
529
|
-
const runtimeVersion = "0.
|
|
559
|
+
const runtimeVersion = "0.4.0";
|
|
530
560
|
//#endregion
|
|
531
561
|
//#region src/config/version-compat.ts
|
|
532
562
|
async function resolveConfigVersionCompatibility(options) {
|
|
@@ -776,7 +806,10 @@ function planToRuntimeSettings(plan, options) {
|
|
|
776
806
|
providers,
|
|
777
807
|
publication: {
|
|
778
808
|
maxInlineComments: plan.publication.maxInlineComments,
|
|
779
|
-
autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id)
|
|
809
|
+
autoResolve: normalizeAutoResolveConfig(plan.publication.autoResolve, defaultProvider.id),
|
|
810
|
+
showHeader: plan.publication.showHeader ?? true,
|
|
811
|
+
showFooter: plan.publication.showFooter ?? true,
|
|
812
|
+
showStats: plan.publication.showStats ?? true
|
|
780
813
|
},
|
|
781
814
|
limits: plan.limits
|
|
782
815
|
},
|
|
@@ -3543,11 +3576,17 @@ function isOfficialInitRecipeId(recipe) {
|
|
|
3543
3576
|
}
|
|
3544
3577
|
//#endregion
|
|
3545
3578
|
//#region src/config/init.ts
|
|
3546
|
-
const supportedOfficialInitAdapters = [
|
|
3547
|
-
|
|
3548
|
-
|
|
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";
|
|
3549
3588
|
function resolveOfficialInitAdapters(adapters) {
|
|
3550
|
-
if (adapters === void 0) return [
|
|
3589
|
+
if (adapters === void 0) return ["github"];
|
|
3551
3590
|
if (adapters.length === 0) return [];
|
|
3552
3591
|
const selected = /* @__PURE__ */ new Set();
|
|
3553
3592
|
for (const adapter of adapters) {
|
|
@@ -3556,7 +3595,7 @@ function resolveOfficialInitAdapters(adapters) {
|
|
|
3556
3595
|
if (adapters.length > 1) throw new Error("Adapter 'none' cannot be mixed with other init adapters.");
|
|
3557
3596
|
return [];
|
|
3558
3597
|
}
|
|
3559
|
-
if (adapter
|
|
3598
|
+
if (!supportedOfficialInitAdapters.includes(adapter)) throw unsupportedAdapterError(adapter);
|
|
3560
3599
|
selected.add(adapter);
|
|
3561
3600
|
}
|
|
3562
3601
|
return [...selected];
|
|
@@ -3627,8 +3666,66 @@ async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false
|
|
|
3627
3666
|
relativePath: path.join(".github", "workflows", "pipr.yml"),
|
|
3628
3667
|
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe, minimal)
|
|
3629
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
|
+
});
|
|
3630
3681
|
return files;
|
|
3631
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
|
+
}
|
|
3632
3729
|
function starterPackageJson() {
|
|
3633
3730
|
return `${JSON.stringify({
|
|
3634
3731
|
private: true,
|
|
@@ -3747,6 +3844,797 @@ function starterWorkflow(relativeConfigDir, recipe, minimal = false) {
|
|
|
3747
3844
|
return lines.join("\n");
|
|
3748
3845
|
}
|
|
3749
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
|
|
3750
4638
|
//#region src/diff/git.ts
|
|
3751
4639
|
function runGit$1(args, cwd, maxBuffer) {
|
|
3752
4640
|
const result = Bun.spawnSync(["git", ...args], {
|
|
@@ -4108,32 +4996,217 @@ function createLocalChangeRequestEvent(options) {
|
|
|
4108
4996
|
});
|
|
4109
4997
|
}
|
|
4110
4998
|
//#endregion
|
|
4111
|
-
//#region src/
|
|
4112
|
-
const
|
|
4113
|
-
|
|
4114
|
-
|
|
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
|
+
};
|
|
4115
5012
|
}
|
|
4116
|
-
function
|
|
4117
|
-
|
|
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;
|
|
4118
5022
|
}
|
|
4119
|
-
function
|
|
4120
|
-
|
|
4121
|
-
const
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
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(" ");
|
|
4137
5210
|
if (!value) return {
|
|
4138
5211
|
ok: false,
|
|
4139
5212
|
error: `Expected '${part}'`
|
|
@@ -4196,7 +5269,7 @@ function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
|
4196
5269
|
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
4197
5270
|
}
|
|
4198
5271
|
//#endregion
|
|
4199
|
-
//#region src/
|
|
5272
|
+
//#region src/host-run/entry-dispatch.ts
|
|
4200
5273
|
const permissionOrder = [
|
|
4201
5274
|
"read",
|
|
4202
5275
|
"triage",
|
|
@@ -4331,229 +5404,44 @@ function renderPlanCommandHelp(plan, reason) {
|
|
|
4331
5404
|
return lines.join("\n");
|
|
4332
5405
|
}
|
|
4333
5406
|
//#endregion
|
|
4334
|
-
//#region src/
|
|
4335
|
-
const
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
};
|
|
4340
|
-
const
|
|
4341
|
-
|
|
4342
|
-
|
|
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));
|
|
4343
5426
|
return {
|
|
4344
|
-
|
|
4345
|
-
|
|
5427
|
+
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
5428
|
+
dataPath,
|
|
5429
|
+
toolNames: piRuntimeReadToolNames
|
|
4346
5430
|
};
|
|
4347
5431
|
}
|
|
4348
|
-
function
|
|
4349
|
-
|
|
4350
|
-
const
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
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");
|
|
4357
5442
|
}
|
|
4358
|
-
function
|
|
4359
|
-
|
|
4360
|
-
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
4361
|
-
const compiled = readCompiledFilter(filter);
|
|
4362
|
-
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
4363
|
-
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
4364
|
-
}
|
|
4365
|
-
function readCompiledFilter(filter) {
|
|
4366
|
-
const existing = compiledFilters.get(filter);
|
|
4367
|
-
if (existing) return existing;
|
|
4368
|
-
const compiled = {
|
|
4369
|
-
include: filter.include?.map(compilePattern),
|
|
4370
|
-
exclude: filter.exclude?.map(compilePattern)
|
|
4371
|
-
};
|
|
4372
|
-
compiledFilters.set(filter, compiled);
|
|
4373
|
-
return compiled;
|
|
4374
|
-
}
|
|
4375
|
-
function compilePattern(pattern) {
|
|
4376
|
-
return {
|
|
4377
|
-
matches: picomatch(pattern, matcherOptions),
|
|
4378
|
-
matchBasename: !pattern.includes("/")
|
|
4379
|
-
};
|
|
4380
|
-
}
|
|
4381
|
-
//#endregion
|
|
4382
|
-
//#region src/diff/manifest-projection.ts
|
|
4383
|
-
const defaultDiffManifestPromptLimits = {
|
|
4384
|
-
fullMaxBytes: 128 * 1024,
|
|
4385
|
-
fullMaxEstimatedTokens: 32e3,
|
|
4386
|
-
condensedMaxBytes: 256 * 1024,
|
|
4387
|
-
condensedMaxEstimatedTokens: 64e3,
|
|
4388
|
-
toolResponseMaxBytes: 64 * 1024
|
|
4389
|
-
};
|
|
4390
|
-
function projectDiffManifest(manifest, options) {
|
|
4391
|
-
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
4392
|
-
const manifestOptions = options ?? {};
|
|
4393
|
-
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
4394
|
-
return parseDiffManifest({
|
|
4395
|
-
...scopedManifest,
|
|
4396
|
-
files: scopedManifest.files.map((file) => ({
|
|
4397
|
-
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
4398
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
4399
|
-
...rangeFieldsForOptions(range, manifestOptions),
|
|
4400
|
-
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
4401
|
-
}))
|
|
4402
|
-
}))
|
|
4403
|
-
});
|
|
4404
|
-
}
|
|
4405
|
-
function cloneDiffManifest(manifest) {
|
|
4406
|
-
return parseDiffManifest(structuredClone(manifest));
|
|
4407
|
-
}
|
|
4408
|
-
function prepareDiffManifestPrompt(manifest, config) {
|
|
4409
|
-
const limits = resolveDiffManifestPromptLimits(config);
|
|
4410
|
-
const full = measureDiffManifestPrompt(manifest);
|
|
4411
|
-
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
4412
|
-
mode: "full",
|
|
4413
|
-
manifest,
|
|
4414
|
-
metrics: {
|
|
4415
|
-
full,
|
|
4416
|
-
selected: full
|
|
4417
|
-
},
|
|
4418
|
-
limits
|
|
4419
|
-
};
|
|
4420
|
-
const condensedManifest = condenseDiffManifest(manifest);
|
|
4421
|
-
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
4422
|
-
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
4423
|
-
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
4424
|
-
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
4425
|
-
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
4426
|
-
].join("; "));
|
|
4427
|
-
return {
|
|
4428
|
-
mode: "condensed",
|
|
4429
|
-
manifest: condensedManifest,
|
|
4430
|
-
metrics: {
|
|
4431
|
-
full,
|
|
4432
|
-
selected: condensed
|
|
4433
|
-
},
|
|
4434
|
-
limits
|
|
4435
|
-
};
|
|
4436
|
-
}
|
|
4437
|
-
function condenseDiffManifest(manifest) {
|
|
4438
|
-
return {
|
|
4439
|
-
baseSha: manifest.baseSha,
|
|
4440
|
-
headSha: manifest.headSha,
|
|
4441
|
-
mergeBaseSha: manifest.mergeBaseSha,
|
|
4442
|
-
files: manifest.files.map(condenseDiffManifestFile)
|
|
4443
|
-
};
|
|
4444
|
-
}
|
|
4445
|
-
function measureDiffManifestPrompt(manifest) {
|
|
4446
|
-
const json = JSON.stringify(manifest, null, 2);
|
|
4447
|
-
const bytes = Buffer.byteLength(json, "utf8");
|
|
4448
|
-
return {
|
|
4449
|
-
bytes,
|
|
4450
|
-
estimatedTokens: Math.ceil(bytes / 4)
|
|
4451
|
-
};
|
|
4452
|
-
}
|
|
4453
|
-
function resolveDiffManifestPromptLimits(config) {
|
|
4454
|
-
return {
|
|
4455
|
-
...defaultDiffManifestPromptLimits,
|
|
4456
|
-
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
4457
|
-
};
|
|
4458
|
-
}
|
|
4459
|
-
function manifestOptionsHaveEffect(options) {
|
|
4460
|
-
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
4461
|
-
}
|
|
4462
|
-
function withoutCompressedFileFields(file, compressed) {
|
|
4463
|
-
if (!compressed) return file;
|
|
4464
|
-
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
4465
|
-
return rest;
|
|
4466
|
-
}
|
|
4467
|
-
function withoutCompressedRangeFields(range, compressed) {
|
|
4468
|
-
if (!compressed) return range;
|
|
4469
|
-
const { summary: _summary, ...rest } = range;
|
|
4470
|
-
return rest;
|
|
4471
|
-
}
|
|
4472
|
-
function rangeFieldsForOptions(range, options) {
|
|
4473
|
-
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
4474
|
-
if (options.includePreviews === false) {
|
|
4475
|
-
const { preview: _preview, ...rest } = fields;
|
|
4476
|
-
return rest;
|
|
4477
|
-
}
|
|
4478
|
-
return fields;
|
|
4479
|
-
}
|
|
4480
|
-
function truncatePreview(preview, maxLines) {
|
|
4481
|
-
if (preview === void 0 || maxLines === void 0) return preview;
|
|
4482
|
-
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
4483
|
-
}
|
|
4484
|
-
function condenseDiffManifestFile(file) {
|
|
4485
|
-
return {
|
|
4486
|
-
path: file.path,
|
|
4487
|
-
previousPath: file.previousPath,
|
|
4488
|
-
status: file.status,
|
|
4489
|
-
language: file.language,
|
|
4490
|
-
additions: file.additions,
|
|
4491
|
-
deletions: file.deletions,
|
|
4492
|
-
hunks: file.hunks.map((hunk) => ({
|
|
4493
|
-
hunkIndex: hunk.hunkIndex,
|
|
4494
|
-
header: hunk.header,
|
|
4495
|
-
oldStart: hunk.oldStart,
|
|
4496
|
-
oldLines: hunk.oldLines,
|
|
4497
|
-
newStart: hunk.newStart,
|
|
4498
|
-
newLines: hunk.newLines,
|
|
4499
|
-
contentHash: hunk.contentHash
|
|
4500
|
-
})),
|
|
4501
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
4502
|
-
id: range.id,
|
|
4503
|
-
path: range.path,
|
|
4504
|
-
side: range.side,
|
|
4505
|
-
startLine: range.startLine,
|
|
4506
|
-
endLine: range.endLine,
|
|
4507
|
-
kind: range.kind,
|
|
4508
|
-
hunkIndex: range.hunkIndex,
|
|
4509
|
-
hunkHeader: range.hunkHeader,
|
|
4510
|
-
hunkContentHash: range.hunkContentHash
|
|
4511
|
-
})),
|
|
4512
|
-
excludedReason: file.excludedReason
|
|
4513
|
-
};
|
|
4514
|
-
}
|
|
4515
|
-
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
4516
|
-
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
4517
|
-
}
|
|
4518
|
-
//#endregion
|
|
4519
|
-
//#region src/pi/runtime-tools.ts
|
|
4520
|
-
const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
|
|
4521
|
-
async function preparePiRuntimeReadTools(options) {
|
|
4522
|
-
const toolRoot = path.join(options.root, "runtime-tools");
|
|
4523
|
-
const baseRoot = path.join(toolRoot, "base");
|
|
4524
|
-
await mkdir(baseRoot, { recursive: true });
|
|
4525
|
-
const baseRanges = await materializeBaseRangeSnapshots({
|
|
4526
|
-
baseRoot,
|
|
4527
|
-
manifest: options.request.manifest,
|
|
4528
|
-
sourceWorkspace: options.sourceWorkspace,
|
|
4529
|
-
maxBytes: options.request.toolResponseMaxBytes
|
|
4530
|
-
});
|
|
4531
|
-
const data = {
|
|
4532
|
-
manifest: options.request.manifest,
|
|
4533
|
-
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
4534
|
-
baseRanges
|
|
4535
|
-
};
|
|
4536
|
-
const dataPath = path.join(toolRoot, "data.json");
|
|
4537
|
-
await Bun.write(dataPath, JSON.stringify(data));
|
|
4538
|
-
return {
|
|
4539
|
-
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
4540
|
-
dataPath,
|
|
4541
|
-
toolNames: piRuntimeReadToolNames
|
|
4542
|
-
};
|
|
4543
|
-
}
|
|
4544
|
-
async function piRuntimeToolsExtensionPath() {
|
|
4545
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
4546
|
-
const candidates = [
|
|
4547
|
-
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
4548
|
-
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
4549
|
-
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
4550
|
-
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
4551
|
-
];
|
|
4552
|
-
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
4553
|
-
throw new Error("Unable to locate pipr runtime tools extension");
|
|
4554
|
-
}
|
|
4555
|
-
async function pathExists(filePath) {
|
|
4556
|
-
return await Bun.file(filePath).exists();
|
|
5443
|
+
async function pathExists(filePath) {
|
|
5444
|
+
return await Bun.file(filePath).exists();
|
|
4557
5445
|
}
|
|
4558
5446
|
async function materializeBaseRangeSnapshots(options) {
|
|
4559
5447
|
const ranges = {};
|
|
@@ -4746,7 +5634,27 @@ const ignoredWorkspacePaths = /* @__PURE__ */ new Set([
|
|
|
4746
5634
|
".fallow",
|
|
4747
5635
|
"coverage"
|
|
4748
5636
|
]);
|
|
4749
|
-
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
|
+
]);
|
|
4750
5658
|
const tokenCountSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
4751
5659
|
const assistantMessageEventSchema = z.looseObject({
|
|
4752
5660
|
type: z.literal("message_end"),
|
|
@@ -4764,6 +5672,14 @@ const assistantUsageMessageSchema = z.looseObject({
|
|
|
4764
5672
|
cost: z.looseObject({ total: z.number().nonnegative() })
|
|
4765
5673
|
})
|
|
4766
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;
|
|
4767
5683
|
async function runPi(options) {
|
|
4768
5684
|
const started = Date.now();
|
|
4769
5685
|
const sandbox = await createPiRunSandbox(options.workspace);
|
|
@@ -4780,19 +5696,13 @@ async function runPi(options) {
|
|
|
4780
5696
|
const promptPath = path.join(sandbox.root, "prompt.md");
|
|
4781
5697
|
await Bun.write(promptPath, options.prompt);
|
|
4782
5698
|
const args = buildPiArgs(options.provider, `@${promptPath}`, sandbox.sessionDir, preparedTools, options.builtinTools);
|
|
4783
|
-
|
|
5699
|
+
return await runProcess(options.piExecutable ?? "pi", args, {
|
|
4784
5700
|
cwd: sandbox.workspace,
|
|
4785
5701
|
env: buildPiEnv(options.provider, sandbox, options.env, preparedTools),
|
|
4786
5702
|
started,
|
|
4787
|
-
timeoutSeconds: options.timeoutSeconds
|
|
5703
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
5704
|
+
streamLimits: options.streamLimits ?? defaultPiStreamLimits
|
|
4788
5705
|
});
|
|
4789
|
-
const events = parsePiJsonEvents(result.stdout);
|
|
4790
|
-
return {
|
|
4791
|
-
...result,
|
|
4792
|
-
...events?.models.length ? { models: events.models } : {},
|
|
4793
|
-
...events?.usage ? { usage: events.usage } : {},
|
|
4794
|
-
stdout: result.exitCode === 0 ? events?.assistantText ?? result.stdout : result.stdout
|
|
4795
|
-
};
|
|
4796
5706
|
} finally {
|
|
4797
5707
|
await preparedTools?.custom?.close();
|
|
4798
5708
|
await chmodRecursive(sandbox.root, 493);
|
|
@@ -4914,69 +5824,195 @@ async function chmodRecursive(target, mode) {
|
|
|
4914
5824
|
const entries = await readdir(target, { withFileTypes: true });
|
|
4915
5825
|
for (const entry of entries) await chmodRecursive(path.join(target, entry.name), mode);
|
|
4916
5826
|
}
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
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;
|
|
4926
5853
|
}
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
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;
|
|
4941
5960
|
const parsed = assistantMessageEventSchema.safeParse(event);
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
function assistantModels(messages) {
|
|
4946
|
-
const models = [];
|
|
4947
|
-
for (const message of messages) {
|
|
5961
|
+
if (!parsed.success) return;
|
|
5962
|
+
const message = parsed.data.message;
|
|
5963
|
+
this.assistantMessageCount += 1;
|
|
4948
5964
|
const model = message.responseModel ?? message.model;
|
|
4949
|
-
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);
|
|
4950
5973
|
}
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
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;
|
|
4973
6015
|
}
|
|
4974
|
-
return {
|
|
4975
|
-
status: partial ? "partial" : "complete",
|
|
4976
|
-
inputTokens,
|
|
4977
|
-
outputTokens,
|
|
4978
|
-
costUsd
|
|
4979
|
-
};
|
|
4980
6016
|
}
|
|
4981
6017
|
function assistantTextFromEvent(event) {
|
|
4982
6018
|
if (event.type === "message_end" || event.type === "turn_end") return assistantMessageText(event.message);
|
|
@@ -5006,7 +6042,9 @@ function textContent(content) {
|
|
|
5006
6042
|
function runProcess(command, args, options) {
|
|
5007
6043
|
return new Promise((resolve, reject) => {
|
|
5008
6044
|
let timedOut = false;
|
|
6045
|
+
let streamFailure;
|
|
5009
6046
|
let timeout;
|
|
6047
|
+
let terminationTimeout;
|
|
5010
6048
|
const detached = process.platform !== "win32";
|
|
5011
6049
|
const child = spawn(command, args, {
|
|
5012
6050
|
cwd: options.cwd,
|
|
@@ -5018,34 +6056,85 @@ function runProcess(command, args, options) {
|
|
|
5018
6056
|
"pipe"
|
|
5019
6057
|
]
|
|
5020
6058
|
});
|
|
5021
|
-
|
|
6059
|
+
const stdout = new PiOutputCollector(options.streamLimits);
|
|
5022
6060
|
let stderr = "";
|
|
6061
|
+
let stderrBytes = 0;
|
|
5023
6062
|
child.stdout.setEncoding("utf8");
|
|
5024
6063
|
child.stderr.setEncoding("utf8");
|
|
5025
6064
|
child.stdout.on("data", (chunk) => {
|
|
5026
|
-
|
|
6065
|
+
const failure = stdout.push(chunk);
|
|
6066
|
+
if (failure) failStream(failure);
|
|
5027
6067
|
});
|
|
5028
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
|
+
}
|
|
5029
6075
|
stderr += chunk;
|
|
6076
|
+
stderrBytes = nextBytes;
|
|
5030
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
|
+
};
|
|
5031
6095
|
if (options.timeoutSeconds !== void 0) timeout = setTimeout(() => {
|
|
5032
6096
|
timedOut = true;
|
|
5033
|
-
|
|
6097
|
+
terminateProcessGroup();
|
|
5034
6098
|
}, options.timeoutSeconds * 1e3);
|
|
5035
6099
|
child.on("error", reject);
|
|
5036
6100
|
child.on("close", (exitCode) => {
|
|
5037
6101
|
if (timeout) clearTimeout(timeout);
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
stdout,
|
|
6102
|
+
resolve(finalizeProcessResult({
|
|
6103
|
+
collector: stdout,
|
|
5041
6104
|
stderr,
|
|
5042
|
-
exitCode
|
|
6105
|
+
exitCode,
|
|
6106
|
+
timedOut,
|
|
6107
|
+
streamFailure,
|
|
6108
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
5043
6109
|
durationMs: Date.now() - options.started
|
|
5044
|
-
});
|
|
6110
|
+
}));
|
|
5045
6111
|
});
|
|
5046
6112
|
});
|
|
5047
6113
|
}
|
|
5048
|
-
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) {
|
|
5049
6138
|
try {
|
|
5050
6139
|
if (process.platform !== "win32" && child.pid) {
|
|
5051
6140
|
process.kill(-child.pid, signal);
|
|
@@ -5057,19 +6146,13 @@ function killProcessGroup(child, signal) {
|
|
|
5057
6146
|
}
|
|
5058
6147
|
}
|
|
5059
6148
|
//#endregion
|
|
5060
|
-
//#region src/shared/redaction.ts
|
|
5061
|
-
const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
|
|
5062
|
-
function redactPotentialSecrets(value) {
|
|
5063
|
-
return value.replace(secretLikeTokenPattern, "[redacted secret]");
|
|
5064
|
-
}
|
|
5065
|
-
//#endregion
|
|
5066
6149
|
//#region src/shared/logging.ts
|
|
5067
6150
|
const sensitiveEnvNamePattern = /(TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL|COOKIE)/i;
|
|
5068
|
-
function
|
|
6151
|
+
function createRuntimeLog(options) {
|
|
5069
6152
|
const secrets = /* @__PURE__ */ new Set();
|
|
5070
6153
|
for (const [key, value] of Object.entries(options.env ?? process.env)) if (sensitiveEnvNamePattern.test(key)) addSecret(secrets, value);
|
|
5071
6154
|
const debugEnabled = (options.env ?? process.env).ACTIONS_STEP_DEBUG === "true" || (options.env ?? process.env).PIPR_LOG_LEVEL === "debug";
|
|
5072
|
-
const sink = options.logSink ??
|
|
6155
|
+
const sink = options.logSink ?? noopRuntimeLogSink;
|
|
5073
6156
|
return {
|
|
5074
6157
|
debugEnabled,
|
|
5075
6158
|
writesToSink: options.logSink !== void 0,
|
|
@@ -5146,7 +6229,7 @@ function emitRecord(sink, secrets, level, event, fields, text) {
|
|
|
5146
6229
|
text
|
|
5147
6230
|
});
|
|
5148
6231
|
}
|
|
5149
|
-
const
|
|
6232
|
+
const noopRuntimeLogSink = {
|
|
5150
6233
|
log() {},
|
|
5151
6234
|
async group(_name, run) {
|
|
5152
6235
|
return await run();
|
|
@@ -5671,7 +6754,8 @@ function logPiResult(options, provider, result, timeoutSeconds) {
|
|
|
5671
6754
|
durationMs: result.durationMs,
|
|
5672
6755
|
stdoutBytes: result.stdout.length,
|
|
5673
6756
|
stderrBytes: result.stderr.length,
|
|
5674
|
-
timeoutSeconds
|
|
6757
|
+
timeoutSeconds,
|
|
6758
|
+
...result.stream ?? {}
|
|
5675
6759
|
});
|
|
5676
6760
|
}
|
|
5677
6761
|
function builtinToolsForPrompt(toolMode) {
|
|
@@ -5756,10 +6840,63 @@ const mainCommentTitles = /* @__PURE__ */ new Set([
|
|
|
5756
6840
|
"# Pipr Review",
|
|
5757
6841
|
mainCommentTitle
|
|
5758
6842
|
]);
|
|
6843
|
+
const mainCommentHeaderHiddenMarker = "<!-- pipr:header:hidden -->";
|
|
6844
|
+
const mainCommentFooterHiddenMarker = "<!-- pipr:footer:hidden -->";
|
|
5759
6845
|
const reviewStatsStartMarker = "<!-- pipr:stats:start -->";
|
|
5760
6846
|
const reviewStatsEndMarker = "<!-- pipr:stats:end -->";
|
|
6847
|
+
const reviewStatsHiddenMarker = "<!-- pipr:stats:hidden -->";
|
|
5761
6848
|
const escapedRepositoryUrl = piprRepositoryUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5762
6849
|
const mainCommentAttributionPattern = new RegExp(`^<sub>Review generated by \\[Pipr\\]\\(${escapedRepositoryUrl}\\) for commit \`[^\`]+\`\\.(?: Config SDK \\d+\\.\\d+\\.\\d+ is behind (?:Pipr \\d+\\.\\d+\\.\\d+|\\[Pipr \\d+\\.\\d+\\.\\d+\\]\\(${escapedRepositoryUrl}/releases/tag/v\\d+\\.\\d+\\.\\d+\\))\\.)?</sub>$`);
|
|
6850
|
+
const maxReviewStatsModelLength = 200;
|
|
6851
|
+
const credentialLikeModelPattern = /(?:(?:AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{16,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})/i;
|
|
6852
|
+
const highEntropyModelSegmentPattern = /[A-Za-z0-9]{24,}/;
|
|
6853
|
+
const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
|
|
6854
|
+
const reviewStatsSchema = z.strictObject({
|
|
6855
|
+
models: z.array(reviewStatsModelSchema).min(1).max(20),
|
|
6856
|
+
agentRuns: z.number().int().positive(),
|
|
6857
|
+
durationMs: z.number().int().nonnegative(),
|
|
6858
|
+
inputTokens: z.number().int().nonnegative(),
|
|
6859
|
+
outputTokens: z.number().int().nonnegative(),
|
|
6860
|
+
costUsd: z.number().nonnegative(),
|
|
6861
|
+
usageStatus: z.enum([
|
|
6862
|
+
"complete",
|
|
6863
|
+
"partial",
|
|
6864
|
+
"unavailable"
|
|
6865
|
+
])
|
|
6866
|
+
});
|
|
6867
|
+
function accumulateReviewStats(prior, current) {
|
|
6868
|
+
if (!prior) return current;
|
|
6869
|
+
if (!current) return prior;
|
|
6870
|
+
const inputTokens = addUsageTotal(prior.inputTokens, current.inputTokens, Number.isSafeInteger);
|
|
6871
|
+
const outputTokens = addUsageTotal(prior.outputTokens, current.outputTokens, Number.isSafeInteger);
|
|
6872
|
+
const costUsd = addUsageTotal(prior.costUsd, current.costUsd, Number.isFinite);
|
|
6873
|
+
const usageStatus = inputTokens.complete && outputTokens.complete && costUsd.complete && prior.usageStatus === current.usageStatus ? prior.usageStatus : "partial";
|
|
6874
|
+
return {
|
|
6875
|
+
models: [.../* @__PURE__ */ new Set([...prior.models, ...current.models])].slice(0, 20),
|
|
6876
|
+
agentRuns: Math.min(Number.MAX_SAFE_INTEGER, prior.agentRuns + current.agentRuns),
|
|
6877
|
+
durationMs: Math.min(Number.MAX_SAFE_INTEGER, prior.durationMs + current.durationMs),
|
|
6878
|
+
inputTokens: inputTokens.total,
|
|
6879
|
+
outputTokens: outputTokens.total,
|
|
6880
|
+
costUsd: costUsd.total,
|
|
6881
|
+
usageStatus
|
|
6882
|
+
};
|
|
6883
|
+
}
|
|
6884
|
+
function addUsageTotal(prior, current, isValid) {
|
|
6885
|
+
const total = prior + current;
|
|
6886
|
+
return isValid(total) && total >= 0 ? {
|
|
6887
|
+
total,
|
|
6888
|
+
complete: true
|
|
6889
|
+
} : {
|
|
6890
|
+
total: prior,
|
|
6891
|
+
complete: false
|
|
6892
|
+
};
|
|
6893
|
+
}
|
|
6894
|
+
function sanitizeReviewStatsModel(model) {
|
|
6895
|
+
const normalized = model.replace(/\s+/g, " ").trim();
|
|
6896
|
+
if (credentialLikeModelPattern.test(normalized) || highEntropyModelSegmentPattern.test(normalized)) return "[redacted credential]";
|
|
6897
|
+
const sanitized = redactPotentialSecrets(normalized);
|
|
6898
|
+
return sanitized ? sanitized.slice(0, maxReviewStatsModelLength) : void 0;
|
|
6899
|
+
}
|
|
5763
6900
|
//#endregion
|
|
5764
6901
|
//#region src/review/prior-state.ts
|
|
5765
6902
|
const mainCommentMarker = "pipr:main-comment";
|
|
@@ -5785,7 +6922,8 @@ const priorReviewStateSchema = z.strictObject({
|
|
|
5785
6922
|
version: z.literal(1),
|
|
5786
6923
|
reviewedHeadSha: z.string().min(1),
|
|
5787
6924
|
selectedTasks: z.array(z.string().min(1)),
|
|
5788
|
-
findings: z.array(priorFindingRecordSchema)
|
|
6925
|
+
findings: z.array(priorFindingRecordSchema),
|
|
6926
|
+
stats: reviewStatsSchema.optional()
|
|
5789
6927
|
});
|
|
5790
6928
|
function buildPriorReviewState(options) {
|
|
5791
6929
|
const scopedPriorState = priorReviewStateForSelectedTasks(options.priorState, options.selectedTasks);
|
|
@@ -5793,6 +6931,7 @@ function buildPriorReviewState(options) {
|
|
|
5793
6931
|
const nextFindings = /* @__PURE__ */ new Map();
|
|
5794
6932
|
const currentFindingIds = /* @__PURE__ */ new Set();
|
|
5795
6933
|
const usedPriorIds = /* @__PURE__ */ new Set();
|
|
6934
|
+
const stats = accumulateReviewStats(scopedPriorState?.stats, options.stats);
|
|
5796
6935
|
for (const finding of options.findings) {
|
|
5797
6936
|
const id = selectFindingId({
|
|
5798
6937
|
finding,
|
|
@@ -5825,7 +6964,8 @@ function buildPriorReviewState(options) {
|
|
|
5825
6964
|
version: 1,
|
|
5826
6965
|
reviewedHeadSha: options.reviewedHeadSha,
|
|
5827
6966
|
selectedTasks: options.selectedTasks,
|
|
5828
|
-
findings: cappedFindings([...nextFindings.values()], currentFindingIds)
|
|
6967
|
+
findings: cappedFindings([...nextFindings.values()], currentFindingIds),
|
|
6968
|
+
...stats ? { stats } : {}
|
|
5829
6969
|
};
|
|
5830
6970
|
}
|
|
5831
6971
|
function resolvePriorFindings(state, findingIds) {
|
|
@@ -6017,35 +7157,13 @@ function parseAttrs(input) {
|
|
|
6017
7157
|
function hashParts(parts) {
|
|
6018
7158
|
return new Bun.CryptoHasher("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
|
|
6019
7159
|
}
|
|
6020
|
-
const maxReviewStatsModelLength = 200;
|
|
6021
|
-
const credentialLikeModelPattern = /(?:(?:AKIA|ASIA)[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|(?:sk|rk)_live_[A-Za-z0-9]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{16,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})/i;
|
|
6022
|
-
const highEntropyModelSegmentPattern = /[A-Za-z0-9]{24,}/;
|
|
6023
|
-
const reviewStatsModelSchema = z.string().min(1).max(maxReviewStatsModelLength).transform((model) => sanitizeReviewStatsModel(model) ?? "[invalid model]");
|
|
6024
|
-
const reviewStatsSchema = z.strictObject({
|
|
6025
|
-
models: z.array(reviewStatsModelSchema).min(1).max(20),
|
|
6026
|
-
agentRuns: z.number().int().positive(),
|
|
6027
|
-
durationMs: z.number().int().nonnegative(),
|
|
6028
|
-
inputTokens: z.number().int().nonnegative(),
|
|
6029
|
-
outputTokens: z.number().int().nonnegative(),
|
|
6030
|
-
costUsd: z.number().nonnegative(),
|
|
6031
|
-
usageStatus: z.enum([
|
|
6032
|
-
"complete",
|
|
6033
|
-
"partial",
|
|
6034
|
-
"unavailable"
|
|
6035
|
-
])
|
|
6036
|
-
});
|
|
6037
|
-
function sanitizeReviewStatsModel(model) {
|
|
6038
|
-
const normalized = model.replace(/\s+/g, " ").trim();
|
|
6039
|
-
if (credentialLikeModelPattern.test(normalized) || highEntropyModelSegmentPattern.test(normalized)) return "[redacted credential]";
|
|
6040
|
-
const sanitized = redactPotentialSecrets(normalized);
|
|
6041
|
-
return sanitized ? sanitized.slice(0, maxReviewStatsModelLength) : void 0;
|
|
6042
|
-
}
|
|
6043
7160
|
//#endregion
|
|
6044
7161
|
//#region src/review/comment.ts
|
|
6045
7162
|
const inlinePublicationItemSchema = z.strictObject({
|
|
6046
7163
|
finding: reviewFindingSchema,
|
|
6047
7164
|
range: commentableRangeSchema,
|
|
6048
7165
|
path: z.string().min(1),
|
|
7166
|
+
previousPath: z.string().min(1).optional(),
|
|
6049
7167
|
side: reviewSideSchema,
|
|
6050
7168
|
startLine: z.number().int().positive(),
|
|
6051
7169
|
endLine: z.number().int().positive(),
|
|
@@ -6080,7 +7198,7 @@ const threadActionSchema = z.strictObject({
|
|
|
6080
7198
|
kind: z.enum(["resolve", "reply"]),
|
|
6081
7199
|
findingId: findingIdSchema,
|
|
6082
7200
|
findingHeadSha: z.string().min(1),
|
|
6083
|
-
commentId: z.
|
|
7201
|
+
commentId: z.string().min(1),
|
|
6084
7202
|
threadId: z.string().min(1).optional(),
|
|
6085
7203
|
body: z.string().min(1),
|
|
6086
7204
|
responseKey: z.string().min(1)
|
|
@@ -6109,6 +7227,20 @@ const publicationPlanSchema = z.strictObject({
|
|
|
6109
7227
|
reviewState: priorReviewStateSchema,
|
|
6110
7228
|
threadActions: threadActionsSchema
|
|
6111
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
|
+
}
|
|
6112
7244
|
function buildPublicationPlan(options) {
|
|
6113
7245
|
const reviewState = options.reviewState ?? buildPriorReviewState({
|
|
6114
7246
|
findings: options.inlineItems.map((item) => item.finding),
|
|
@@ -6125,7 +7257,10 @@ function buildPublicationPlan(options) {
|
|
|
6125
7257
|
event: options.event,
|
|
6126
7258
|
reviewState,
|
|
6127
7259
|
main: options.main,
|
|
6128
|
-
metadata
|
|
7260
|
+
metadata,
|
|
7261
|
+
showHeader: options.showHeader ?? true,
|
|
7262
|
+
showFooter: options.showFooter ?? true,
|
|
7263
|
+
showStats: options.showStats ?? true
|
|
6129
7264
|
}),
|
|
6130
7265
|
mainMarker: mainCommentMarker,
|
|
6131
7266
|
changeNumber: options.event.change.number,
|
|
@@ -6138,19 +7273,21 @@ function buildPublicationPlan(options) {
|
|
|
6138
7273
|
function preparePublishableInlineFindings(options) {
|
|
6139
7274
|
const ranges = createDiffRangeIndex(options.manifest);
|
|
6140
7275
|
return options.validated.validFindings.flatMap((finding) => {
|
|
6141
|
-
const
|
|
6142
|
-
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;
|
|
6143
7279
|
const findingWithBody = findingWithPublishableBody(finding);
|
|
6144
7280
|
if (!findingWithBody) return [];
|
|
6145
7281
|
return [{
|
|
6146
7282
|
finding: findingWithPublishableSuggestedFix(findingWithBody, range),
|
|
6147
|
-
range
|
|
7283
|
+
range,
|
|
7284
|
+
previousPath: file.previousPath
|
|
6148
7285
|
}];
|
|
6149
7286
|
});
|
|
6150
7287
|
}
|
|
6151
7288
|
function prepareInlinePublicationItemsForPublishableFindings(options) {
|
|
6152
7289
|
const seenFindingIds = /* @__PURE__ */ new Set();
|
|
6153
|
-
return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range }) => {
|
|
7290
|
+
return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath }) => {
|
|
6154
7291
|
const findingId = findingIdFor(publishableFinding, options.reviewState);
|
|
6155
7292
|
const stateRecord = options.reviewState ? matchFindingRecord(options.reviewState, publishableFinding) : void 0;
|
|
6156
7293
|
if (seenFindingIds.has(findingId) || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
|
|
@@ -6160,6 +7297,7 @@ function prepareInlinePublicationItemsForPublishableFindings(options) {
|
|
|
6160
7297
|
finding: publishableFinding,
|
|
6161
7298
|
range,
|
|
6162
7299
|
path: publishableFinding.path,
|
|
7300
|
+
previousPath,
|
|
6163
7301
|
side: publishableFinding.side,
|
|
6164
7302
|
startLine: publishableFinding.startLine,
|
|
6165
7303
|
endLine: publishableFinding.endLine,
|
|
@@ -6209,14 +7347,14 @@ function renderMainComment(options) {
|
|
|
6209
7347
|
reviewState: options.reviewState
|
|
6210
7348
|
}),
|
|
6211
7349
|
"",
|
|
6212
|
-
|
|
6213
|
-
"",
|
|
7350
|
+
...!options.showHeader ? [mainCommentHeaderHiddenMarker, ""] : [],
|
|
7351
|
+
...options.showHeader ? [mainCommentTitle, ""] : [],
|
|
6214
7352
|
...options.metadata.validFindings > 0 ? [`**Findings:** ${options.metadata.validFindings}`, ""] : [],
|
|
6215
7353
|
redactPotentialSecrets(options.main),
|
|
6216
7354
|
"",
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
""
|
|
7355
|
+
...!options.showStats || !options.metadata.stats ? [reviewStatsHiddenMarker, ""] : [],
|
|
7356
|
+
...options.showStats && options.metadata.stats ? [renderReviewStats(options.metadata.stats), ""] : [],
|
|
7357
|
+
...options.showFooter ? [renderMainCommentAttribution(options.metadata), ""] : [mainCommentFooterHiddenMarker, ""]
|
|
6220
7358
|
].join("\n");
|
|
6221
7359
|
}
|
|
6222
7360
|
function renderReviewStats(stats) {
|
|
@@ -6307,7 +7445,8 @@ function buildCommentPublishingPlan(options) {
|
|
|
6307
7445
|
priorState: options.priorReviewState,
|
|
6308
7446
|
findings: publishableInlineFindings.map((item) => item.finding),
|
|
6309
7447
|
reviewedHeadSha: options.event.change.head.sha,
|
|
6310
|
-
selectedTasks: options.metadata.selectedTasks
|
|
7448
|
+
selectedTasks: options.metadata.selectedTasks,
|
|
7449
|
+
stats: options.metadata.stats
|
|
6311
7450
|
});
|
|
6312
7451
|
const inlineCommentDrafts = prepareInlinePublicationItemsForPublishableFindings({
|
|
6313
7452
|
publishableFindings: publishableInlineFindings,
|
|
@@ -6319,7 +7458,13 @@ function buildCommentPublishingPlan(options) {
|
|
|
6319
7458
|
main: options.main,
|
|
6320
7459
|
inlineItems: inlineCommentDrafts,
|
|
6321
7460
|
maxInlineComments: options.maxInlineComments,
|
|
6322
|
-
|
|
7461
|
+
showHeader: options.showHeader,
|
|
7462
|
+
showFooter: options.showFooter,
|
|
7463
|
+
showStats: options.showStats,
|
|
7464
|
+
metadata: {
|
|
7465
|
+
...options.metadata,
|
|
7466
|
+
...reviewState.stats ? { stats: reviewState.stats } : {}
|
|
7467
|
+
},
|
|
6323
7468
|
reviewState,
|
|
6324
7469
|
threadActions: options.threadActions
|
|
6325
7470
|
});
|
|
@@ -6555,8 +7700,12 @@ function internalVerifierAgent(provider, config) {
|
|
|
6555
7700
|
"Do not repeat user-supplied text unless needed to explain the verdict.",
|
|
6556
7701
|
config.publication.autoResolve.instructions
|
|
6557
7702
|
].filter(Boolean).join("\n"),
|
|
6558
|
-
prompt: (input) =>
|
|
7703
|
+
prompt: (input) => {
|
|
7704
|
+
const { manifest: _manifest, ...verifierPromptInput } = input;
|
|
7705
|
+
return JSON.stringify(verifierPromptInput, null, 2);
|
|
7706
|
+
},
|
|
6559
7707
|
tools: [],
|
|
7708
|
+
timeout: "2m",
|
|
6560
7709
|
retry: {
|
|
6561
7710
|
invalidOutput: 1,
|
|
6562
7711
|
transientFailure: 0
|
|
@@ -6762,19 +7911,34 @@ function priorReviewForTask(priorMainComment, priorReviewState) {
|
|
|
6762
7911
|
}
|
|
6763
7912
|
function visibleMainComment(body) {
|
|
6764
7913
|
const sourceLines = body.split("\n");
|
|
6765
|
-
const
|
|
6766
|
-
const lines = sourceLines.filter((
|
|
6767
|
-
return !(statsRange && index >= statsRange.start && index <= statsRange.end) &&
|
|
7914
|
+
const envelope = parseGeneratedMainCommentEnvelope(sourceLines);
|
|
7915
|
+
const lines = sourceLines.filter((_line, index) => {
|
|
7916
|
+
return !(envelope.statsRange && index >= envelope.statsRange.start && index <= envelope.statsRange.end) && index !== envelope.mainMarkerIndex && index !== envelope.headerMarkerIndex && index !== envelope.statsMarkerIndex && index !== envelope.footerIndex;
|
|
6768
7917
|
});
|
|
6769
7918
|
while (lines[0] === "") lines.shift();
|
|
6770
|
-
if (lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
|
|
7919
|
+
if (envelope.headerMarkerIndex < 0 && lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
|
|
6771
7920
|
while (lines[0] === "") lines.shift();
|
|
6772
7921
|
return lines.join("\n").trim();
|
|
6773
7922
|
}
|
|
6774
|
-
function
|
|
6775
|
-
const
|
|
6776
|
-
|
|
6777
|
-
const
|
|
7923
|
+
function parseGeneratedMainCommentEnvelope(lines) {
|
|
7924
|
+
const mainMarkerIndex = lines.findIndex((line) => line.startsWith("<!-- pipr:main-comment "));
|
|
7925
|
+
const headerCandidateOffset = lines.slice(mainMarkerIndex + 1).findIndex((line) => line !== "");
|
|
7926
|
+
const headerCandidateIndex = mainMarkerIndex + 1 + headerCandidateOffset;
|
|
7927
|
+
const headerMarkerIndex = mainMarkerIndex >= 0 && headerCandidateOffset >= 0 && lines[headerCandidateIndex] === "<!-- pipr:header:hidden -->" ? headerCandidateIndex : -1;
|
|
7928
|
+
const lastLineIndex = lines.findLastIndex((line) => line !== "");
|
|
7929
|
+
const lastLine = lines[lastLineIndex] ?? "";
|
|
7930
|
+
const footerIndex = lastLine === "<!-- pipr:footer:hidden -->" || mainCommentAttributionPattern.test(lastLine) ? lastLineIndex : -1;
|
|
7931
|
+
const lastContentIndex = lines.slice(0, footerIndex < 0 ? lines.length : footerIndex).findLastIndex((line) => line !== "");
|
|
7932
|
+
return {
|
|
7933
|
+
mainMarkerIndex,
|
|
7934
|
+
headerMarkerIndex,
|
|
7935
|
+
statsMarkerIndex: lines[lastContentIndex] === "<!-- pipr:stats:hidden -->" ? lastContentIndex : -1,
|
|
7936
|
+
statsRange: generatedReviewStatsRange(lines, footerIndex),
|
|
7937
|
+
footerIndex
|
|
7938
|
+
};
|
|
7939
|
+
}
|
|
7940
|
+
function generatedReviewStatsRange(lines, generatedFooterIndex) {
|
|
7941
|
+
const end = lines.slice(0, generatedFooterIndex < 0 ? lines.length : generatedFooterIndex).findLastIndex((line) => line !== "");
|
|
6778
7942
|
if (end < 0) return;
|
|
6779
7943
|
if (lines[end] !== "<!-- pipr:stats:end -->") return;
|
|
6780
7944
|
if (lines[end - 1] !== "</details>") return;
|
|
@@ -6970,6 +8134,9 @@ async function runTaskRuntime(options) {
|
|
|
6970
8134
|
validated,
|
|
6971
8135
|
manifest: diffManifest,
|
|
6972
8136
|
maxInlineComments: config.publication.maxInlineComments,
|
|
8137
|
+
showHeader: config.publication.showHeader,
|
|
8138
|
+
showFooter: config.publication.showFooter,
|
|
8139
|
+
showStats: config.publication.showStats,
|
|
6973
8140
|
priorReviewState: verifier.priorReviewState,
|
|
6974
8141
|
threadActions: verifier.threadActions,
|
|
6975
8142
|
metadata: {
|
|
@@ -7092,107 +8259,1105 @@ function createTaskContext(options) {
|
|
|
7092
8259
|
async reply(markdown) {
|
|
7093
8260
|
collectCommandResponse(options.output, markdown, options.taskName);
|
|
7094
8261
|
}
|
|
7095
|
-
} : void 0,
|
|
7096
|
-
secret(secret) {
|
|
7097
|
-
return resolveTaskSecret(secret, options);
|
|
8262
|
+
} : void 0,
|
|
8263
|
+
secret(secret) {
|
|
8264
|
+
return resolveTaskSecret(secret, options);
|
|
8265
|
+
},
|
|
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
|
+
}
|
|
7098
8920
|
},
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
runtime: {
|
|
7105
|
-
...options,
|
|
7106
|
-
taskContext,
|
|
7107
|
-
runId: options.runId,
|
|
7108
|
-
piRunSink: options.piRunSink
|
|
7109
|
-
}
|
|
7110
|
-
});
|
|
7111
|
-
options.output.providerModels.push(...result.providerModels);
|
|
7112
|
-
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
7113
|
-
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
7114
|
-
return agentOutputForTaskContext(agent, result.value);
|
|
7115
|
-
} },
|
|
7116
|
-
review: { async prior() {
|
|
7117
|
-
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
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);
|
|
7118
8926
|
} },
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
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
|
+
})
|
|
7122
8941
|
},
|
|
7123
|
-
|
|
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
|
+
}
|
|
7124
8979
|
};
|
|
7125
|
-
return taskContext;
|
|
7126
8980
|
}
|
|
7127
|
-
function
|
|
7128
|
-
|
|
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;
|
|
7129
8984
|
}
|
|
7130
|
-
function
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
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
|
+
};
|
|
7136
8993
|
}
|
|
7137
|
-
|
|
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();
|
|
7138
9023
|
return {
|
|
7139
|
-
kind: "
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
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
|
|
7149
9037
|
}
|
|
7150
9038
|
};
|
|
7151
9039
|
}
|
|
7152
|
-
function
|
|
7153
|
-
const
|
|
7154
|
-
const
|
|
7155
|
-
|
|
7156
|
-
|
|
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
|
|
7157
9063
|
};
|
|
7158
|
-
|
|
7159
|
-
review,
|
|
7160
|
-
|
|
7161
|
-
|
|
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
|
+
}
|
|
7162
9076
|
};
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
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
|
|
7180
9100
|
}
|
|
7181
|
-
}
|
|
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() {
|
|
7182
9114
|
return {
|
|
7183
|
-
kind: "
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
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
|
+
}
|
|
7194
9355
|
};
|
|
7195
9356
|
}
|
|
9357
|
+
function bitbucketCoordinates(change) {
|
|
9358
|
+
if (change.coordinates?.provider !== "bitbucket") throw new Error("Bitbucket adapter requires Bitbucket coordinates");
|
|
9359
|
+
return change.coordinates;
|
|
9360
|
+
}
|
|
7196
9361
|
//#endregion
|
|
7197
9362
|
//#region src/shared/github.ts
|
|
7198
9363
|
const githubApiVersion = "2026-03-10";
|
|
@@ -7207,12 +9372,21 @@ function parseRepoSlug(value) {
|
|
|
7207
9372
|
repo
|
|
7208
9373
|
};
|
|
7209
9374
|
}
|
|
9375
|
+
function githubCoordinates(value) {
|
|
9376
|
+
const { owner, repo } = parseRepoSlug(value);
|
|
9377
|
+
return {
|
|
9378
|
+
provider: "github",
|
|
9379
|
+
owner,
|
|
9380
|
+
repository: repo
|
|
9381
|
+
};
|
|
9382
|
+
}
|
|
7210
9383
|
//#endregion
|
|
7211
9384
|
//#region src/hosts/github/command.ts
|
|
7212
9385
|
const pullRequestDetailsSchema = z.looseObject({
|
|
7213
9386
|
title: z.string().optional(),
|
|
7214
9387
|
body: z.string().nullable().optional(),
|
|
7215
9388
|
html_url: z.string().optional(),
|
|
9389
|
+
draft: z.boolean().optional(),
|
|
7216
9390
|
user: z.looseObject({ login: z.string().min(1).optional() }).optional(),
|
|
7217
9391
|
base: z.looseObject({
|
|
7218
9392
|
sha: z.string().min(1),
|
|
@@ -7235,7 +9409,7 @@ const pullRequestDetailsSchema = z.looseObject({
|
|
|
7235
9409
|
});
|
|
7236
9410
|
const githubRepositoryPermissionResponseSchema = z.looseObject({
|
|
7237
9411
|
permission: z.string().min(1),
|
|
7238
|
-
role_name: z.string().
|
|
9412
|
+
role_name: z.string().optional()
|
|
7239
9413
|
});
|
|
7240
9414
|
const permissionLevels = /* @__PURE__ */ new Set([
|
|
7241
9415
|
"read",
|
|
@@ -7305,7 +9479,8 @@ function githubPullRequestDetails(options) {
|
|
|
7305
9479
|
author: githubActor(options.json.head.user?.login),
|
|
7306
9480
|
fork: options.json.head.repo?.fork
|
|
7307
9481
|
},
|
|
7308
|
-
isFork: githubHeadIsFork(options.json, repository.slug)
|
|
9482
|
+
isFork: githubHeadIsFork(options.json, repository.slug),
|
|
9483
|
+
isDraft: options.json.draft
|
|
7309
9484
|
}
|
|
7310
9485
|
};
|
|
7311
9486
|
}
|
|
@@ -7332,6 +9507,7 @@ const githubPullRequestPayloadSchema = z.looseObject({
|
|
|
7332
9507
|
title: z.string().optional(),
|
|
7333
9508
|
body: z.string().nullable().optional(),
|
|
7334
9509
|
html_url: z.string().optional(),
|
|
9510
|
+
draft: z.boolean().optional(),
|
|
7335
9511
|
user: z.looseObject({ login: z.string().optional() }).optional(),
|
|
7336
9512
|
base: z.looseObject({
|
|
7337
9513
|
sha: z.string().optional(),
|
|
@@ -7392,7 +9568,7 @@ const githubIssueCommentEventContextSchema = z.strictObject({
|
|
|
7392
9568
|
url: z.string().min(1).optional()
|
|
7393
9569
|
}),
|
|
7394
9570
|
changeNumber: z.number({ error: "GitHub issue comment event is missing issue number" }).int().positive(),
|
|
7395
|
-
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),
|
|
7396
9572
|
isChangeRequest: z.boolean(),
|
|
7397
9573
|
body: z.string(),
|
|
7398
9574
|
actor: z.string({ error: "GitHub issue comment event is missing comment user" }).min(1),
|
|
@@ -7407,8 +9583,8 @@ const githubReviewCommentReplyEventSchema = z.strictObject({
|
|
|
7407
9583
|
url: z.string().min(1).optional()
|
|
7408
9584
|
}),
|
|
7409
9585
|
changeNumber: z.number({ error: "GitHub review comment event is missing pull request number" }).int().positive(),
|
|
7410
|
-
commentId: z.number({ error: "GitHub review comment event is missing comment id" }).int().positive(),
|
|
7411
|
-
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(),
|
|
7412
9588
|
body: z.string(),
|
|
7413
9589
|
actor: z.string({ error: "GitHub review comment event is missing comment user" }).min(1),
|
|
7414
9590
|
workspace: z.string().min(1)
|
|
@@ -7427,18 +9603,21 @@ function githubPullRequestEventInput(payload, options) {
|
|
|
7427
9603
|
host: options.env.GITHUB_SERVER_URL
|
|
7428
9604
|
},
|
|
7429
9605
|
repository,
|
|
9606
|
+
coordinates: githubCoordinates(repository.slug),
|
|
7430
9607
|
change: githubEventChange(payload, repository.slug),
|
|
7431
9608
|
workspace: options.workspace
|
|
7432
9609
|
};
|
|
7433
9610
|
}
|
|
7434
9611
|
function githubEventRepository(payload, env) {
|
|
7435
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");
|
|
7436
9619
|
return {
|
|
7437
|
-
slug
|
|
7438
|
-
payload.repository?.full_name,
|
|
7439
|
-
baseRepo?.full_name,
|
|
7440
|
-
env.GITHUB_REPOSITORY
|
|
7441
|
-
].find((value) => value !== void 0 && value.length > 0),
|
|
9620
|
+
slug,
|
|
7442
9621
|
url: payload.repository?.html_url ?? baseRepo?.html_url
|
|
7443
9622
|
};
|
|
7444
9623
|
}
|
|
@@ -7452,7 +9631,8 @@ function githubEventChange(payload, repositorySlug) {
|
|
|
7452
9631
|
author: githubActor(pullRequest.user?.login),
|
|
7453
9632
|
base: githubBaseEndpoint(pullRequest),
|
|
7454
9633
|
head: githubHeadEndpoint(pullRequest),
|
|
7455
|
-
isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug)
|
|
9634
|
+
isFork: githubEventHeadIsFork(pullRequest.head?.repo?.full_name, repositorySlug),
|
|
9635
|
+
isDraft: pullRequest.draft
|
|
7456
9636
|
};
|
|
7457
9637
|
}
|
|
7458
9638
|
function githubBaseEndpoint(pullRequest) {
|
|
@@ -7750,16 +9930,6 @@ const githubResolveReviewThreadMutation = `
|
|
|
7750
9930
|
}
|
|
7751
9931
|
`;
|
|
7752
9932
|
//#endregion
|
|
7753
|
-
//#region src/review/publication-result.ts
|
|
7754
|
-
/** Error thrown when publication fails after producing partial result metadata. */
|
|
7755
|
-
var PublicationError = class extends Error {
|
|
7756
|
-
result;
|
|
7757
|
-
constructor(message, result) {
|
|
7758
|
-
super(message);
|
|
7759
|
-
this.result = result;
|
|
7760
|
-
}
|
|
7761
|
-
};
|
|
7762
|
-
//#endregion
|
|
7763
9933
|
//#region src/hosts/github/publication-shared.ts
|
|
7764
9934
|
async function assertCurrentHeadSha(client, change, reviewedHeadSha) {
|
|
7765
9935
|
const headMismatch = await currentHeadShaMismatch(client, change, reviewedHeadSha);
|
|
@@ -7816,21 +9986,25 @@ async function publishGitHubCommandResponse(options) {
|
|
|
7816
9986
|
repo: options.change.repository.slug,
|
|
7817
9987
|
issueNumber: options.change.change.number
|
|
7818
9988
|
}), marker, ownerLogin);
|
|
7819
|
-
if (existing)
|
|
7820
|
-
|
|
7821
|
-
id: (await options.client.updateIssueComment({
|
|
9989
|
+
if (existing) {
|
|
9990
|
+
const updated = await options.client.updateIssueComment({
|
|
7822
9991
|
repo: options.change.repository.slug,
|
|
7823
9992
|
commentId: existing.id,
|
|
7824
9993
|
body
|
|
7825
|
-
})
|
|
7826
|
-
|
|
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
|
+
});
|
|
7827
10005
|
return {
|
|
7828
10006
|
action: "created",
|
|
7829
|
-
id: (
|
|
7830
|
-
repo: options.change.repository.slug,
|
|
7831
|
-
issueNumber: options.change.change.number,
|
|
7832
|
-
body
|
|
7833
|
-
})).id
|
|
10007
|
+
id: String(created.id)
|
|
7834
10008
|
};
|
|
7835
10009
|
}
|
|
7836
10010
|
function findCommandResponseComment(comments, marker, ownerLogin) {
|
|
@@ -7873,14 +10047,14 @@ async function loadGitHubInlineThreadContexts(options) {
|
|
|
7873
10047
|
return [{
|
|
7874
10048
|
findingId: marker.id,
|
|
7875
10049
|
findingHeadSha: marker.head,
|
|
7876
|
-
parentCommentId: comment.id,
|
|
10050
|
+
parentCommentId: String(comment.id),
|
|
7877
10051
|
parentBody: comment.body ?? "",
|
|
7878
10052
|
threadId: thread?.id,
|
|
7879
10053
|
threadResolved: thread?.isResolved ?? false,
|
|
7880
10054
|
comments: thread?.commentIds.flatMap((id) => {
|
|
7881
10055
|
const item = commentById.get(id);
|
|
7882
10056
|
return item ? [{
|
|
7883
|
-
id: item.id,
|
|
10057
|
+
id: String(item.id),
|
|
7884
10058
|
body: item.body ?? "",
|
|
7885
10059
|
authorLogin: item.authorLogin
|
|
7886
10060
|
}] : [];
|
|
@@ -8001,7 +10175,7 @@ async function postThreadActionReplyIfNeeded(context, action) {
|
|
|
8001
10175
|
await context.client.createReviewCommentReply({
|
|
8002
10176
|
repo: context.change.repository.slug,
|
|
8003
10177
|
pullRequestNumber: context.change.change.number,
|
|
8004
|
-
commentId: action.commentId,
|
|
10178
|
+
commentId: Number(action.commentId),
|
|
8005
10179
|
body: [
|
|
8006
10180
|
marker.body,
|
|
8007
10181
|
"",
|
|
@@ -8073,7 +10247,7 @@ async function resolveReviewThread(context, action) {
|
|
|
8073
10247
|
}
|
|
8074
10248
|
function threadForAction(context, action) {
|
|
8075
10249
|
if (action.kind !== "resolve") return;
|
|
8076
|
-
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));
|
|
8077
10251
|
}
|
|
8078
10252
|
//#endregion
|
|
8079
10253
|
//#region src/hosts/github/publication-review.ts
|
|
@@ -8121,21 +10295,25 @@ async function upsertMainComment(options) {
|
|
|
8121
10295
|
repo: options.change.repository.slug,
|
|
8122
10296
|
issueNumber: options.change.change.number
|
|
8123
10297
|
}), options.plan.mainMarker, options.change.change.number, options.ownerLogin);
|
|
8124
|
-
if (existing)
|
|
8125
|
-
|
|
8126
|
-
id: (await options.client.updateIssueComment({
|
|
10298
|
+
if (existing) {
|
|
10299
|
+
const updated = await options.client.updateIssueComment({
|
|
8127
10300
|
repo: options.change.repository.slug,
|
|
8128
10301
|
commentId: existing.id,
|
|
8129
10302
|
body: options.plan.mainComment
|
|
8130
|
-
})
|
|
8131
|
-
|
|
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
|
+
});
|
|
8132
10314
|
return {
|
|
8133
10315
|
action: "created",
|
|
8134
|
-
id: (
|
|
8135
|
-
repo: options.change.repository.slug,
|
|
8136
|
-
issueNumber: options.change.change.number,
|
|
8137
|
-
body: options.plan.mainComment
|
|
8138
|
-
})).id
|
|
10316
|
+
id: String(created.id)
|
|
8139
10317
|
};
|
|
8140
10318
|
}
|
|
8141
10319
|
async function publishInlineComments(options) {
|
|
@@ -8291,48 +10469,18 @@ function gitConfigEnvCount(env) {
|
|
|
8291
10469
|
const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? process.env.GIT_CONFIG_COUNT ?? Bun.env.GIT_CONFIG_COUNT ?? "0", 10);
|
|
8292
10470
|
return Number.isSafeInteger(count) && count >= 0 ? count : 0;
|
|
8293
10471
|
}
|
|
8294
|
-
function gitGlobalConfigHome(env) {
|
|
8295
|
-
if (existsSync("/home/bun")) return "/home/bun";
|
|
8296
|
-
const root = env.RUNNER_TEMP ?? env.TMPDIR ?? os.tmpdir();
|
|
8297
|
-
mkdirSync(root, { recursive: true });
|
|
8298
|
-
return root;
|
|
8299
|
-
}
|
|
8300
|
-
function ensureGitHubHeadCheckout(options) {
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
"--depth=1",
|
|
8306
|
-
"origin",
|
|
8307
|
-
`refs/pull/${options.change.change.number}/head`
|
|
8308
|
-
]);
|
|
8309
|
-
if (runGit(options.rootDir, ["rev-parse", "HEAD"]).trim() !== headSha) runGit(options.rootDir, [
|
|
8310
|
-
"checkout",
|
|
8311
|
-
"--detach",
|
|
8312
|
-
headSha
|
|
8313
|
-
]);
|
|
8314
|
-
}
|
|
8315
|
-
function hasGitCommit(rootDir, sha) {
|
|
8316
|
-
try {
|
|
8317
|
-
runGit(rootDir, [
|
|
8318
|
-
"cat-file",
|
|
8319
|
-
"-e",
|
|
8320
|
-
`${sha}^{commit}`
|
|
8321
|
-
]);
|
|
8322
|
-
return true;
|
|
8323
|
-
} catch {
|
|
8324
|
-
return false;
|
|
8325
|
-
}
|
|
8326
|
-
}
|
|
8327
|
-
function runGit(rootDir, args) {
|
|
8328
|
-
const result = Bun.spawnSync(["git", ...args], {
|
|
8329
|
-
cwd: rootDir,
|
|
8330
|
-
env: process.env,
|
|
8331
|
-
stderr: "pipe",
|
|
8332
|
-
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`
|
|
8333
10483
|
});
|
|
8334
|
-
if (result.exitCode !== 0) throw new Error(result.stderr.toString().trim() || `git ${args.join(" ")} failed`);
|
|
8335
|
-
return result.stdout.toString();
|
|
8336
10484
|
}
|
|
8337
10485
|
//#endregion
|
|
8338
10486
|
//#region src/hosts/github/adapter.ts
|
|
@@ -8342,35 +10490,63 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8342
10490
|
const publicationClient = options.publicationClient ?? createGitHubPublicationClient(env);
|
|
8343
10491
|
return {
|
|
8344
10492
|
id: "github",
|
|
10493
|
+
capabilities: {
|
|
10494
|
+
commandComments: true,
|
|
10495
|
+
reviewCommentReplies: true,
|
|
10496
|
+
threadResolution: true,
|
|
10497
|
+
multilineInlineComments: true,
|
|
10498
|
+
suggestedChanges: true,
|
|
10499
|
+
statuses: true
|
|
10500
|
+
},
|
|
8345
10501
|
events: {
|
|
8346
|
-
parseEvent(parseOptions) {
|
|
8347
|
-
|
|
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
|
+
};
|
|
8348
10525
|
},
|
|
8349
10526
|
async loadChangeRequest(ref) {
|
|
10527
|
+
const loaded = await commandClient.getPullRequest({
|
|
10528
|
+
repository: ref.repository,
|
|
10529
|
+
changeNumber: ref.changeNumber
|
|
10530
|
+
});
|
|
8350
10531
|
return {
|
|
8351
|
-
...
|
|
8352
|
-
|
|
8353
|
-
changeNumber: ref.changeNumber
|
|
8354
|
-
}),
|
|
10532
|
+
...loaded,
|
|
10533
|
+
coordinates: githubCoordinates(loaded.repository.slug),
|
|
8355
10534
|
eventName: ref.eventName,
|
|
8356
10535
|
action: ref.action,
|
|
8357
10536
|
rawAction: ref.rawAction,
|
|
8358
10537
|
workspace: ref.workspace
|
|
8359
10538
|
};
|
|
8360
|
-
},
|
|
8361
|
-
resolveCommandComment(parseOptions) {
|
|
8362
|
-
return loadGitHubIssueCommentEventContext(parseOptions);
|
|
8363
|
-
},
|
|
8364
|
-
resolveReviewCommentReply(parseOptions) {
|
|
8365
|
-
return loadGitHubReviewCommentReplyEvent(parseOptions);
|
|
8366
10539
|
}
|
|
8367
10540
|
},
|
|
8368
10541
|
workspace: {
|
|
8369
10542
|
ensureHeadCheckout: ensureGitHubHeadCheckout,
|
|
8370
10543
|
ensureWorkspaceSafeDirectory: ensureGitHubWorkspaceSafeDirectory
|
|
8371
10544
|
},
|
|
8372
|
-
permissions: { getRepositoryPermission(
|
|
8373
|
-
return commandClient.getRepositoryPermission(
|
|
10545
|
+
permissions: { getRepositoryPermission({ change, actor }) {
|
|
10546
|
+
return commandClient.getRepositoryPermission({
|
|
10547
|
+
repository: change.repository,
|
|
10548
|
+
actor
|
|
10549
|
+
});
|
|
8374
10550
|
} },
|
|
8375
10551
|
publication: {
|
|
8376
10552
|
publish(options) {
|
|
@@ -8384,7 +10560,7 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8384
10560
|
return publishGitHubCommandResponse({
|
|
8385
10561
|
client: publicationClient,
|
|
8386
10562
|
change: options.change,
|
|
8387
|
-
sourceCommentId: options.sourceCommentId,
|
|
10563
|
+
sourceCommentId: Number(options.sourceCommentId),
|
|
8388
10564
|
commandName: options.commandName,
|
|
8389
10565
|
body: options.body
|
|
8390
10566
|
});
|
|
@@ -8418,44 +10594,509 @@ function createGitHubHostAdapter(options = {}) {
|
|
|
8418
10594
|
});
|
|
8419
10595
|
}
|
|
8420
10596
|
},
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
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({
|
|
8424
10616
|
repo: options.change.repository.slug,
|
|
8425
|
-
|
|
8426
|
-
|
|
10617
|
+
checkRunId: Number(options.status.id),
|
|
10618
|
+
name: options.status.name,
|
|
10619
|
+
conclusion: options.state,
|
|
8427
10620
|
summary: options.summary
|
|
8428
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"
|
|
8429
10686
|
},
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
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)
|
|
8437
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
|
+
};
|
|
8438
11030
|
}
|
|
8439
11031
|
}
|
|
8440
11032
|
};
|
|
8441
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
|
+
}
|
|
8442
11076
|
//#endregion
|
|
8443
|
-
//#region src/
|
|
8444
|
-
function
|
|
11077
|
+
//#region src/host-run/adapter.ts
|
|
11078
|
+
function assertTrustedHostRunProviderEnv(options, trustedConfig) {
|
|
8445
11079
|
const env = options.env ?? process.env;
|
|
8446
11080
|
const missing = [];
|
|
8447
11081
|
for (const provider of trustedConfig.providers) if (!env[provider.apiKeyEnv]) missing.push(provider.apiKeyEnv);
|
|
8448
11082
|
if (missing.length > 0) throw new Error(`Missing provider env vars: ${missing.join(", ")}`);
|
|
8449
11083
|
}
|
|
8450
|
-
function
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
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
|
|
8455
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 });
|
|
8456
11097
|
}
|
|
8457
11098
|
//#endregion
|
|
8458
|
-
//#region src/
|
|
11099
|
+
//#region src/host-run/logging.ts
|
|
8459
11100
|
async function logPhase(log, name, run) {
|
|
8460
11101
|
const started = Date.now();
|
|
8461
11102
|
log.info(`${name} start`);
|
|
@@ -8504,8 +11145,8 @@ function addProviderSecrets(log, config, env) {
|
|
|
8504
11145
|
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
8505
11146
|
}
|
|
8506
11147
|
//#endregion
|
|
8507
|
-
//#region src/
|
|
8508
|
-
const genericCheckFailureSummary = "pipr failed; see
|
|
11148
|
+
//#region src/host-run/runtime-checks.ts
|
|
11149
|
+
const genericCheckFailureSummary = "pipr failed; see runner logs for details.";
|
|
8509
11150
|
async function startRuntimeChecks(options) {
|
|
8510
11151
|
if (!canStartRuntimeChecks(options)) return;
|
|
8511
11152
|
const tasks = options.selectedTasks;
|
|
@@ -8528,8 +11169,8 @@ async function startRuntimeChecks(options) {
|
|
|
8528
11169
|
try {
|
|
8529
11170
|
await startTaskCheckRuns(started);
|
|
8530
11171
|
if (aggregateName) {
|
|
8531
|
-
started.aggregate = await
|
|
8532
|
-
started.log?.info("
|
|
11172
|
+
started.aggregate = await createStatusOrThrow(started, aggregateName, "pipr review is running.");
|
|
11173
|
+
started.log?.info("status created", {
|
|
8533
11174
|
name: aggregateName,
|
|
8534
11175
|
kind: "aggregate"
|
|
8535
11176
|
});
|
|
@@ -8541,7 +11182,7 @@ async function startRuntimeChecks(options) {
|
|
|
8541
11182
|
return started;
|
|
8542
11183
|
}
|
|
8543
11184
|
async function finalizeRuntimeChecks(checks, options) {
|
|
8544
|
-
if (!checks?.adapter.
|
|
11185
|
+
if (!checks?.adapter.statuses) return;
|
|
8545
11186
|
const taskResults = checks.tasks.map((task) => taskCheckResultForFinalization(task, checks.outcomes.get(task.name), options));
|
|
8546
11187
|
const taskError = await updateTaskCheckRuns(checks, taskResults);
|
|
8547
11188
|
const aggregateError = await updateAggregateCheckRun(checks, taskResults, options);
|
|
@@ -8549,37 +11190,38 @@ async function finalizeRuntimeChecks(checks, options) {
|
|
|
8549
11190
|
if (firstError !== void 0) throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
8550
11191
|
}
|
|
8551
11192
|
function canStartRuntimeChecks(options) {
|
|
8552
|
-
return options.
|
|
11193
|
+
return options.taskName === void 0 && options.adapter.capabilities.statuses && Boolean(options.adapter.statuses?.isAvailable(options.event));
|
|
8553
11194
|
}
|
|
8554
11195
|
async function startTaskCheckRuns(started) {
|
|
8555
11196
|
for (const task of started.tasks) {
|
|
8556
11197
|
const settings = taskCheckSettings(task);
|
|
8557
11198
|
if (!settings.individual) continue;
|
|
8558
|
-
const run = await
|
|
11199
|
+
const run = await createStatusOrThrow(started, settings.name, "pipr task is running.");
|
|
8559
11200
|
started.taskRuns.set(task.name, run);
|
|
8560
|
-
started.log?.info("
|
|
11201
|
+
started.log?.info("status created", {
|
|
8561
11202
|
name: settings.name,
|
|
8562
11203
|
kind: "task",
|
|
8563
11204
|
task: task.name
|
|
8564
11205
|
});
|
|
8565
11206
|
}
|
|
8566
11207
|
}
|
|
8567
|
-
async function
|
|
11208
|
+
async function createStatusOrThrow(checks, name, summary) {
|
|
8568
11209
|
try {
|
|
8569
|
-
const
|
|
8570
|
-
if (!
|
|
8571
|
-
return await
|
|
11210
|
+
const statuses = checks.adapter.statuses;
|
|
11211
|
+
if (!statuses) throw new Error("status publication is not available");
|
|
11212
|
+
return await statuses.upsert({
|
|
8572
11213
|
change: checks.event,
|
|
8573
11214
|
name,
|
|
11215
|
+
state: "pending",
|
|
8574
11216
|
summary
|
|
8575
11217
|
});
|
|
8576
11218
|
} catch (error) {
|
|
8577
|
-
throw
|
|
11219
|
+
throw statusPermissionError(error);
|
|
8578
11220
|
}
|
|
8579
11221
|
}
|
|
8580
|
-
function
|
|
11222
|
+
function statusPermissionError(error) {
|
|
8581
11223
|
const message = error instanceof Error ? error.message : String(error);
|
|
8582
|
-
return /* @__PURE__ */ new Error(`Unable to
|
|
11224
|
+
return /* @__PURE__ */ new Error(`Unable to publish code host status. Check the adapter credential scopes. ${message}`);
|
|
8583
11225
|
}
|
|
8584
11226
|
async function updateTaskCheckRuns(checks, taskResults) {
|
|
8585
11227
|
let firstError;
|
|
@@ -8597,20 +11239,21 @@ async function updateAggregateCheckRun(checks, taskResults, options) {
|
|
|
8597
11239
|
}
|
|
8598
11240
|
async function updateCheckRunOrError(checks, checkRun, result) {
|
|
8599
11241
|
try {
|
|
8600
|
-
await checks.adapter.
|
|
11242
|
+
await checks.adapter.statuses?.upsert({
|
|
8601
11243
|
change: checks.event,
|
|
8602
|
-
checkRun,
|
|
8603
|
-
|
|
11244
|
+
name: checkRun.name,
|
|
11245
|
+
status: checkRun,
|
|
11246
|
+
state: result.conclusion,
|
|
8604
11247
|
summary: result.summary
|
|
8605
11248
|
});
|
|
8606
|
-
checks.log?.info("
|
|
11249
|
+
checks.log?.info("status finalized", {
|
|
8607
11250
|
name: checkRun.name,
|
|
8608
11251
|
conclusion: result.conclusion,
|
|
8609
11252
|
summary: result.summary
|
|
8610
11253
|
});
|
|
8611
11254
|
return;
|
|
8612
11255
|
} catch (error) {
|
|
8613
|
-
checks.log?.warning("
|
|
11256
|
+
checks.log?.warning("status finalize failed", {
|
|
8614
11257
|
name: checkRun.name,
|
|
8615
11258
|
error: error instanceof Error ? error.message : String(error)
|
|
8616
11259
|
});
|
|
@@ -8677,7 +11320,7 @@ function taskCheckSettings(task) {
|
|
|
8677
11320
|
};
|
|
8678
11321
|
}
|
|
8679
11322
|
//#endregion
|
|
8680
|
-
//#region src/
|
|
11323
|
+
//#region src/host-run/review-publishing.ts
|
|
8681
11324
|
async function runTrustedReviewAndPublish(options) {
|
|
8682
11325
|
const checks = await startRuntimeChecks({
|
|
8683
11326
|
adapter: options.adapter,
|
|
@@ -8728,13 +11371,14 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
8728
11371
|
const publish = options.adapter.publication?.publish;
|
|
8729
11372
|
if (!publish) throw new Error("review publication is not available for this code host");
|
|
8730
11373
|
const publication = await options.log.group("publish review", async () => {
|
|
11374
|
+
const publicationPlan = publicationPlanForHostCapabilities(review.publicationPlan, options.adapter.capabilities);
|
|
8731
11375
|
options.log.info("publication plan", {
|
|
8732
|
-
inlineItems:
|
|
8733
|
-
threadActions:
|
|
11376
|
+
inlineItems: publicationPlan.inlineItems.length,
|
|
11377
|
+
threadActions: publicationPlan.threadActions.length
|
|
8734
11378
|
});
|
|
8735
11379
|
const result = await publish({
|
|
8736
11380
|
change: options.event,
|
|
8737
|
-
plan:
|
|
11381
|
+
plan: publicationPlan
|
|
8738
11382
|
});
|
|
8739
11383
|
options.log.notice("publication result", {
|
|
8740
11384
|
main: result.mainComment.action,
|
|
@@ -8762,7 +11406,7 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
8762
11406
|
}
|
|
8763
11407
|
}
|
|
8764
11408
|
//#endregion
|
|
8765
|
-
//#region src/
|
|
11409
|
+
//#region src/host-run/git-project.ts
|
|
8766
11410
|
async function loadRuntimeProjectFromGitCommit(options) {
|
|
8767
11411
|
const configDir = resolveContainedConfigDir(options);
|
|
8768
11412
|
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
@@ -8841,7 +11485,7 @@ function showFileAtCommit(rootDir, commitSha, filePath) {
|
|
|
8841
11485
|
return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
|
|
8842
11486
|
}
|
|
8843
11487
|
//#endregion
|
|
8844
|
-
//#region src/
|
|
11488
|
+
//#region src/host-run/trusted-runtime.ts
|
|
8845
11489
|
async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
8846
11490
|
const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
|
|
8847
11491
|
rootDir: options.rootDir,
|
|
@@ -8854,30 +11498,78 @@ async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
|
8854
11498
|
}
|
|
8855
11499
|
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
8856
11500
|
addProviderSecrets(log, config, options.env);
|
|
8857
|
-
|
|
11501
|
+
assertTrustedHostRunProviderEnv(options, config);
|
|
8858
11502
|
await logPhase(log, "checkout head", async () => {
|
|
8859
|
-
adapter.workspace.ensureHeadCheckout({
|
|
11503
|
+
await adapter.workspace.ensureHeadCheckout({
|
|
8860
11504
|
rootDir: options.rootDir,
|
|
8861
11505
|
change: event
|
|
8862
11506
|
});
|
|
8863
11507
|
});
|
|
8864
11508
|
}
|
|
8865
11509
|
//#endregion
|
|
8866
|
-
//#region src/
|
|
8867
|
-
async function
|
|
8868
|
-
|
|
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);
|
|
8869
11566
|
if (prepared.kind === "ignored") {
|
|
8870
|
-
log.notice("
|
|
11567
|
+
log.notice("event ignored", { reason: prepared.reason });
|
|
8871
11568
|
return prepared;
|
|
8872
11569
|
}
|
|
8873
11570
|
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
8874
11571
|
}
|
|
8875
|
-
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
8876
|
-
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
8877
|
-
eventPath: options.eventPath,
|
|
8878
|
-
env: options.env ?? process.env,
|
|
8879
|
-
workspace: options.rootDir
|
|
8880
|
-
}));
|
|
11572
|
+
async function prepareIssueCommentCommand(options, adapter, log, comment) {
|
|
8881
11573
|
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
8882
11574
|
if (runnable.kind === "ignored") return runnable;
|
|
8883
11575
|
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
@@ -8894,6 +11586,7 @@ async function prepareIssueCommentCommand(options, adapter, log) {
|
|
|
8894
11586
|
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
8895
11587
|
platform: { id: adapter.id },
|
|
8896
11588
|
repository: loaded.repository,
|
|
11589
|
+
coordinates: loaded.coordinates,
|
|
8897
11590
|
change: loaded.change,
|
|
8898
11591
|
workspace: loaded.workspace ?? comment.workspace
|
|
8899
11592
|
});
|
|
@@ -8938,7 +11631,7 @@ function runnableIssueCommentCommand(comment, dryRun) {
|
|
|
8938
11631
|
async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
|
|
8939
11632
|
const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
|
|
8940
11633
|
const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
|
|
8941
|
-
|
|
11634
|
+
change: prepared.event,
|
|
8942
11635
|
actor: prepared.comment.actor
|
|
8943
11636
|
}));
|
|
8944
11637
|
log.notice("command dispatch", {
|
|
@@ -9008,7 +11701,7 @@ async function issueCommentCommandResult(options) {
|
|
|
9008
11701
|
kind: "ignored",
|
|
9009
11702
|
reason: options.completed.reason
|
|
9010
11703
|
};
|
|
9011
|
-
if (options.completed.kind === "command-response") return await
|
|
11704
|
+
if (options.completed.kind === "command-response") return await publishCommandResponseHostRunResult({
|
|
9012
11705
|
adapter: options.adapter,
|
|
9013
11706
|
completed: options.completed,
|
|
9014
11707
|
event: options.event,
|
|
@@ -9024,7 +11717,7 @@ async function issueCommentCommandResult(options) {
|
|
|
9024
11717
|
publication: options.completed.publication
|
|
9025
11718
|
};
|
|
9026
11719
|
}
|
|
9027
|
-
async function
|
|
11720
|
+
async function publishCommandResponseHostRunResult(options) {
|
|
9028
11721
|
const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
|
|
9029
11722
|
if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
|
|
9030
11723
|
const publication = await publishCommandResponse({
|
|
@@ -9043,76 +11736,21 @@ async function publishCommandResponseActionResult(options) {
|
|
|
9043
11736
|
};
|
|
9044
11737
|
}
|
|
9045
11738
|
//#endregion
|
|
9046
|
-
//#region src/
|
|
9047
|
-
async function
|
|
9048
|
-
const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
|
|
9049
|
-
eventPath: options.eventPath,
|
|
9050
|
-
env: options.env ?? process.env,
|
|
9051
|
-
workspace: options.rootDir
|
|
9052
|
-
}));
|
|
9053
|
-
logEventContext(log, event);
|
|
9054
|
-
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
9055
|
-
if (options.dryRun) {
|
|
9056
|
-
log.notice("dry run stop before review runtime, model, or GitHub publishing calls");
|
|
9057
|
-
return {
|
|
9058
|
-
kind: "dry-run",
|
|
9059
|
-
event,
|
|
9060
|
-
configSource: trustedRuntime.settings.source
|
|
9061
|
-
};
|
|
9062
|
-
}
|
|
9063
|
-
await prepareTrustedHeadCheckout(options, adapter, trustedRuntime.settings.config, event, log);
|
|
9064
|
-
const dispatch = dispatchRuntimeEntry({
|
|
9065
|
-
kind: "change-request",
|
|
9066
|
-
plan: trustedRuntime.plan,
|
|
9067
|
-
event
|
|
9068
|
-
});
|
|
9069
|
-
const selectedTasks = dispatch.kind === "change-request" ? dispatch.tasks : [];
|
|
9070
|
-
log.notice("dispatch", { selectedTasks: selectedTasks.map((task) => task.name) });
|
|
9071
|
-
const completed = await runTrustedReviewAndPublish({
|
|
9072
|
-
options,
|
|
9073
|
-
adapter,
|
|
9074
|
-
trustedRuntime,
|
|
9075
|
-
event,
|
|
9076
|
-
selectedTasks,
|
|
9077
|
-
log
|
|
9078
|
-
});
|
|
9079
|
-
if (completed.kind === "skipped") {
|
|
9080
|
-
log.notice("action ignored", { reason: completed.reason });
|
|
9081
|
-
return {
|
|
9082
|
-
kind: "ignored",
|
|
9083
|
-
reason: completed.reason
|
|
9084
|
-
};
|
|
9085
|
-
}
|
|
9086
|
-
if (completed.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
9087
|
-
return {
|
|
9088
|
-
kind: "review",
|
|
9089
|
-
event,
|
|
9090
|
-
configSource: trustedRuntime.settings.source,
|
|
9091
|
-
review: completed.review,
|
|
9092
|
-
publication: completed.publication
|
|
9093
|
-
};
|
|
9094
|
-
}
|
|
9095
|
-
//#endregion
|
|
9096
|
-
//#region src/action/verifier-entry.ts
|
|
9097
|
-
async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
11739
|
+
//#region src/host-run/verifier-entry.ts
|
|
11740
|
+
async function runReviewCommentReplyHostRunCommand(options, adapter, log, reply) {
|
|
9098
11741
|
const capabilities = reviewCommentReplyDispatchCapabilities(options, adapter);
|
|
9099
11742
|
if (capabilities.kind === "ignored") {
|
|
9100
|
-
log.notice("
|
|
11743
|
+
log.notice("event ignored", { reason: capabilities.reason });
|
|
9101
11744
|
return capabilities;
|
|
9102
11745
|
}
|
|
9103
|
-
const reply = await logPhase(log, "parse review comment reply", async () => capabilities.resolveReviewCommentReply({
|
|
9104
|
-
eventPath: options.eventPath,
|
|
9105
|
-
env: options.env ?? process.env,
|
|
9106
|
-
workspace: options.rootDir
|
|
9107
|
-
}));
|
|
9108
11746
|
const runnable = runnableReviewCommentReply(reply);
|
|
9109
11747
|
if (runnable.kind === "ignored") {
|
|
9110
|
-
log.notice("
|
|
11748
|
+
log.notice("event ignored", { reason: runnable.reason });
|
|
9111
11749
|
return runnable;
|
|
9112
11750
|
}
|
|
9113
11751
|
const prepared = await prepareReviewCommentVerifier(options, adapter, reply, log);
|
|
9114
11752
|
if (prepared.kind === "ignored") {
|
|
9115
|
-
log.notice("
|
|
11753
|
+
log.notice("event ignored", { reason: prepared.reason });
|
|
9116
11754
|
return prepared;
|
|
9117
11755
|
}
|
|
9118
11756
|
const result = await runReviewCommentVerifier(options, adapter, prepared, log);
|
|
@@ -9133,9 +11771,9 @@ async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
|
9133
11771
|
};
|
|
9134
11772
|
}
|
|
9135
11773
|
function reviewCommentReplyDispatchCapabilities(options, adapter) {
|
|
9136
|
-
if (!adapter.
|
|
11774
|
+
if (!adapter.capabilities.reviewCommentReplies || !adapter.capabilities.threadResolution) return {
|
|
9137
11775
|
kind: "ignored",
|
|
9138
|
-
reason: "host adapter does not support
|
|
11776
|
+
reason: "host adapter does not support verifier replies"
|
|
9139
11777
|
};
|
|
9140
11778
|
if (!adapter.publication?.publishThreadActions) return {
|
|
9141
11779
|
kind: "ignored",
|
|
@@ -9147,7 +11785,6 @@ function reviewCommentReplyDispatchCapabilities(options, adapter) {
|
|
|
9147
11785
|
};
|
|
9148
11786
|
return {
|
|
9149
11787
|
kind: "ready",
|
|
9150
|
-
resolveReviewCommentReply: adapter.events.resolveReviewCommentReply,
|
|
9151
11788
|
publishThreadActions: adapter.publication.publishThreadActions
|
|
9152
11789
|
};
|
|
9153
11790
|
}
|
|
@@ -9170,6 +11807,7 @@ async function prepareReviewCommentVerifier(options, adapter, reply, log) {
|
|
|
9170
11807
|
rawAction: loaded.rawAction ?? reply.rawAction,
|
|
9171
11808
|
platform: { id: adapter.id },
|
|
9172
11809
|
repository: loaded.repository,
|
|
11810
|
+
coordinates: loaded.coordinates,
|
|
9173
11811
|
change: loaded.change,
|
|
9174
11812
|
workspace: loaded.workspace ?? reply.workspace
|
|
9175
11813
|
});
|
|
@@ -9275,12 +11913,12 @@ async function verifierActorAllowed(adapter, event, reply, config) {
|
|
|
9275
11913
|
if (allowed === "any") return true;
|
|
9276
11914
|
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
9277
11915
|
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
9278
|
-
|
|
11916
|
+
change: event,
|
|
9279
11917
|
actor: reply.actor
|
|
9280
11918
|
}), "write");
|
|
9281
11919
|
}
|
|
9282
11920
|
//#endregion
|
|
9283
|
-
//#region src/
|
|
11921
|
+
//#region src/host-run/commands.ts
|
|
9284
11922
|
/** Initializes the official minimal `.pipr` project files. */
|
|
9285
11923
|
async function runInitCommand(options) {
|
|
9286
11924
|
return await initOfficialMinimalProject({
|
|
@@ -9310,21 +11948,19 @@ async function runInspectCommand(options) {
|
|
|
9310
11948
|
warnings: runtime.settings.warnings
|
|
9311
11949
|
};
|
|
9312
11950
|
}
|
|
9313
|
-
/** Loads the runtime config and
|
|
11951
|
+
/** Loads the runtime config and change request event without running review publication. */
|
|
9314
11952
|
async function runDryRunCommand(options) {
|
|
9315
11953
|
const runtime = await loadRuntimeProject({
|
|
9316
11954
|
...options,
|
|
9317
11955
|
requireProviderEnv: false
|
|
9318
11956
|
});
|
|
9319
|
-
const
|
|
11957
|
+
const hostEvent = await createHostRunAdapter(options).events.parseEvent({
|
|
9320
11958
|
eventPath: options.eventPath,
|
|
9321
|
-
env:
|
|
9322
|
-
...options.env,
|
|
9323
|
-
GITHUB_WORKSPACE: options.rootDir,
|
|
9324
|
-
GITHUB_EVENT_NAME: "pull_request"
|
|
9325
|
-
},
|
|
11959
|
+
env: options.env ?? process.env,
|
|
9326
11960
|
workspace: options.rootDir
|
|
9327
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;
|
|
9328
11964
|
return {
|
|
9329
11965
|
configSource: runtime.settings.source,
|
|
9330
11966
|
event,
|
|
@@ -9333,7 +11969,7 @@ async function runDryRunCommand(options) {
|
|
|
9333
11969
|
}
|
|
9334
11970
|
/** Runs configured change-request tasks against local Git base and head revisions. */
|
|
9335
11971
|
async function runLocalReviewCommand(options) {
|
|
9336
|
-
const log = options.logSink ?
|
|
11972
|
+
const log = options.logSink ? createRuntimeLog({
|
|
9337
11973
|
logSink: options.logSink,
|
|
9338
11974
|
env: options.env
|
|
9339
11975
|
}) : void 0;
|
|
@@ -9397,36 +12033,43 @@ async function runLocalReviewCommand(options) {
|
|
|
9397
12033
|
});
|
|
9398
12034
|
return result;
|
|
9399
12035
|
}
|
|
9400
|
-
/** Runs
|
|
9401
|
-
async function
|
|
9402
|
-
return await
|
|
12036
|
+
/** Runs a normalized code host event through the selected adapter. */
|
|
12037
|
+
async function runHostRunCommand(options) {
|
|
12038
|
+
return await runHostRunCommandWithDependencies(options);
|
|
9403
12039
|
}
|
|
9404
|
-
async function
|
|
9405
|
-
const log =
|
|
12040
|
+
async function runHostRunCommandWithDependencies(options) {
|
|
12041
|
+
const log = createRuntimeLog({
|
|
9406
12042
|
logSink: options.logSink,
|
|
9407
12043
|
env: options.env
|
|
9408
12044
|
});
|
|
9409
|
-
return await log.group("pipr
|
|
9410
|
-
|
|
9411
|
-
log.notice("action start", {
|
|
9412
|
-
eventName,
|
|
12045
|
+
return await log.group("pipr host run", async () => {
|
|
12046
|
+
log.notice("host run start", {
|
|
9413
12047
|
dryRun: options.dryRun,
|
|
9414
12048
|
root: options.rootDir,
|
|
9415
12049
|
configDir: options.configDir
|
|
9416
12050
|
});
|
|
9417
|
-
const adapter =
|
|
12051
|
+
const adapter = createHostRunAdapter(options);
|
|
9418
12052
|
await logPhase(log, "workspace", async () => {
|
|
9419
12053
|
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
9420
12054
|
rootDir: options.rootDir,
|
|
9421
12055
|
env: options.env
|
|
9422
12056
|
});
|
|
9423
12057
|
});
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
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
|
+
}
|
|
9427
12070
|
});
|
|
9428
12071
|
}
|
|
9429
12072
|
//#endregion
|
|
9430
|
-
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 };
|
|
9431
12074
|
|
|
9432
|
-
//# sourceMappingURL=commands-
|
|
12075
|
+
//# sourceMappingURL=commands-ZZkXwqEn.mjs.map
|