@usepipr/runtime 0.3.8 → 0.4.0

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