@sentry/junior-github 0.161.0 → 0.163.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.
@@ -1,3 +1,3 @@
1
1
  import type { ConversationAnnotation, ConversationSidebarAnnotation } from "@sentry/junior-plugin-api";
2
- /** Select the one GitHub annotation summary shown in a conversation row. */
3
- export declare function githubSidebarAnnotation(annotations: ConversationAnnotation[]): ConversationSidebarAnnotation | undefined;
2
+ /** Return GitHub annotations for a conversation row, newest first. */
3
+ export declare function githubSidebarAnnotations(annotations: ConversationAnnotation[]): ConversationSidebarAnnotation[];
package/dist/index.js CHANGED
@@ -730,12 +730,28 @@ import {
730
730
  pluginToolOutputSchema
731
731
  } from "@sentry/junior-plugin-api";
732
732
  import { z } from "zod";
733
- var RESERVED_SANDBOX_DIRECTORIES = /* @__PURE__ */ new Set([".junior", "data", "skills"]);
733
+
734
+ // src/sandbox-paths.ts
735
+ var RESERVED_SANDBOX_DIRECTORIES = /* @__PURE__ */ new Set([
736
+ ".junior",
737
+ "data",
738
+ "skills"
739
+ ]);
740
+ function isReservedSandboxDirectory(path) {
741
+ return RESERVED_SANDBOX_DIRECTORIES.has(path.toLowerCase());
742
+ }
743
+
744
+ // src/tools/clone-repository.ts
734
745
  var inputSchema = z.object({
735
746
  repo: z.string().describe('Repository in "owner/name" format.'),
736
- directory: z.string().regex(/^[A-Za-z0-9._-]+$/).refine((value) => value !== "." && value !== "..", {
737
- message: "Directory must be a single directory name."
738
- }).describe("Optional destination directory under the sandbox root.").optional()
747
+ directory: z.string().regex(/^(?:[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/).refine(
748
+ (value) => !value.split("/").some((part) => part === "." || part === ".."),
749
+ {
750
+ message: "Directory must be a relative path without . or .. segments."
751
+ }
752
+ ).describe(
753
+ "Optional destination directory under the sandbox root. Defaults to repos/{name}."
754
+ ).optional()
739
755
  }).strict();
740
756
  var cloneSchema = z.object({
741
757
  path: z.string(),
@@ -753,7 +769,7 @@ function parseRepo(value) {
753
769
  return { owner: parts[0], name: parts[1] };
754
770
  }
755
771
  function defaultDirectory(repoName) {
756
- return RESERVED_SANDBOX_DIRECTORIES.has(repoName) ? `${repoName}-repo` : repoName;
772
+ return `repos/${repoName}`;
757
773
  }
758
774
  function commandSignal(signal, timeoutMs) {
759
775
  const timeout = AbortSignal.timeout(timeoutMs);
@@ -802,7 +818,27 @@ function createGitHubCloneRepositoryTool(ctx) {
802
818
  async execute(input, options) {
803
819
  const repo = parseRepo(input.repo);
804
820
  const directory = input.directory ?? defaultDirectory(repo.name);
821
+ const rootSegment = directory.split("/")[0] ?? directory;
822
+ if (isReservedSandboxDirectory(rootSegment)) {
823
+ throw new PluginToolInputError(
824
+ `Directory conflicts with a reserved sandbox path: ${directory}`
825
+ );
826
+ }
805
827
  const path = `${ctx.sandbox.root}/${directory}`;
828
+ const parentDirectory = directory.includes("/") ? directory.slice(0, directory.lastIndexOf("/")) : void 0;
829
+ if (parentDirectory) {
830
+ const mkdir = await ctx.sandbox.run({
831
+ cmd: "mkdir",
832
+ args: ["-p", "--", `${ctx.sandbox.root}/${parentDirectory}`],
833
+ cwd: ctx.sandbox.root,
834
+ signal: commandSignal(options.signal, 3e4)
835
+ });
836
+ if (mkdir.exitCode !== 0) {
837
+ throw new PluginToolInputError(
838
+ `Failed to create clone parent directory: ${parentDirectory}`
839
+ );
840
+ }
841
+ }
806
842
  const exists = await ctx.sandbox.run({
807
843
  cmd: "bash",
808
844
  args: ["-c", `test -e "$1"`, "bash", path],
@@ -4506,13 +4542,6 @@ async function classifyGitHubPullRequestCommitComposition(args) {
4506
4542
  }
4507
4543
 
4508
4544
  // src/annotations.ts
4509
- var STATUS_RANK = {
4510
- warning: 5,
4511
- open: 4,
4512
- draft: 3,
4513
- merged: 2,
4514
- closed: 1
4515
- };
4516
4545
  var STATUS_ICON = {
4517
4546
  warning: "triangle-alert",
4518
4547
  open: "circle-dot",
@@ -4520,31 +4549,40 @@ var STATUS_ICON = {
4520
4549
  merged: "git-merge",
4521
4550
  closed: "circle-x"
4522
4551
  };
4523
- function repositoryScope(annotation) {
4552
+ function isPullRequestUrl(url) {
4553
+ try {
4554
+ return /^\/[^/]+\/[^/]+\/pull\/\d+(?:\/|$)/.test(new URL(url).pathname);
4555
+ } catch {
4556
+ return false;
4557
+ }
4558
+ }
4559
+ function sidebarIconForStatus(status, url) {
4560
+ if (status === "open" && isPullRequestUrl(url)) return "git-pull-request";
4561
+ return STATUS_ICON[status];
4562
+ }
4563
+ function repositoryName(annotation) {
4524
4564
  try {
4525
- const [, owner, repo] = new URL(annotation.url).pathname.split("/");
4526
- return owner && repo ? { key: `${owner}/${repo}`, label: repo } : void 0;
4565
+ const [, , repo] = new URL(annotation.url).pathname.split("/");
4566
+ return repo || void 0;
4527
4567
  } catch {
4528
4568
  return void 0;
4529
4569
  }
4530
4570
  }
4531
- function githubSidebarAnnotation(annotations) {
4532
- const links = annotations.flatMap((annotation) => {
4533
- const repo = repositoryScope(annotation);
4534
- const status2 = annotation.status;
4535
- return repo && status2 ? [{ repo, status: status2 }] : [];
4536
- });
4537
- if (links.length === 0) return void 0;
4538
- const repos = new Map(links.map((link) => [link.repo.key, link.repo.label]));
4539
- const status = links.reduce(
4540
- (current, link) => STATUS_RANK[link.status] > STATUS_RANK[current] ? link.status : current,
4541
- "closed"
4542
- );
4543
- return {
4544
- icon: STATUS_ICON[status],
4545
- key: "github",
4546
- label: repos.size === 1 ? [...repos.values()][0] : `${repos.size} repos`
4547
- };
4571
+ function githubSidebarAnnotations(annotations) {
4572
+ return annotations.flatMap((annotation) => {
4573
+ const status = annotation.status;
4574
+ const label = repositoryName(annotation);
4575
+ return status && label ? [
4576
+ {
4577
+ annotation: {
4578
+ icon: sidebarIconForStatus(status, annotation.url),
4579
+ key: annotation.key,
4580
+ label
4581
+ },
4582
+ updatedAt: annotation.updatedAt
4583
+ }
4584
+ ] : [];
4585
+ }).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map(({ annotation }) => annotation);
4548
4586
  }
4549
4587
 
4550
4588
  // src/webhooks/check-suite-enrichment.ts
@@ -4909,6 +4947,63 @@ function linkifyGitHubReferences(text2) {
4909
4947
  }).join("\n");
4910
4948
  }
4911
4949
 
4950
+ // src/workspace-prepare.ts
4951
+ async function prepareWorkspace(ctx) {
4952
+ const repos = ctx.repos.map((entry) => {
4953
+ const [owner, name, ...rest] = entry.repo.split("/");
4954
+ if (!owner || !name || rest.length > 0) {
4955
+ throw new Error(`Invalid GitHub repository: ${entry.repo}`);
4956
+ }
4957
+ const segments = entry.path.split("/");
4958
+ if (segments.length === 0 || segments.some(
4959
+ (part) => !part || part === "." || part === ".." || !/^[A-Za-z0-9._-]+$/.test(part)
4960
+ ) || isReservedSandboxDirectory(segments[0])) {
4961
+ throw new Error(`Invalid workspace checkout path: ${entry.path}`);
4962
+ }
4963
+ return { owner, name, path: entry.path, repo: entry.repo };
4964
+ });
4965
+ const paths = /* @__PURE__ */ new Set();
4966
+ for (const entry of repos) {
4967
+ const key = entry.path.toLowerCase();
4968
+ if (paths.has(key)) {
4969
+ throw new Error(`Workspace checkout path collision: ${entry.path}`);
4970
+ }
4971
+ paths.add(key);
4972
+ }
4973
+ for (const { owner, name, path, repo } of repos) {
4974
+ const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : void 0;
4975
+ if (parent) {
4976
+ const mkdir = await ctx.sandbox.run({
4977
+ cmd: "mkdir",
4978
+ args: ["-p", "--", parent],
4979
+ cwd: ctx.sandbox.root
4980
+ });
4981
+ if (mkdir.exitCode !== 0) {
4982
+ throw new Error(
4983
+ `GitHub workspace checkout parent failed for ${repo}: ${mkdir.stderr.trim() || `exit ${mkdir.exitCode}`}`
4984
+ );
4985
+ }
4986
+ }
4987
+ const result = await ctx.sandbox.run({
4988
+ cmd: "git",
4989
+ args: [
4990
+ "clone",
4991
+ "--quiet",
4992
+ "--depth=1",
4993
+ "--",
4994
+ `https://github.com/${owner}/${name}.git`,
4995
+ path
4996
+ ],
4997
+ cwd: ctx.sandbox.root
4998
+ });
4999
+ if (result.exitCode !== 0) {
5000
+ throw new Error(
5001
+ `GitHub workspace clone failed for ${repo}: ${result.stderr.trim() || `exit ${result.exitCode}`}`
5002
+ );
5003
+ }
5004
+ }
5005
+ }
5006
+
4912
5007
  // src/plugin.ts
4913
5008
  function githubSmartHttpAccess(upstreamUrl) {
4914
5009
  const pathname = upstreamUrl.pathname.toLowerCase();
@@ -5361,10 +5456,10 @@ function githubPlugin(options = {}) {
5361
5456
  return {
5362
5457
  annotationsByConversationId: Object.fromEntries(
5363
5458
  ctx.conversationIds.flatMap((conversationId) => {
5364
- const annotation = githubSidebarAnnotation(
5459
+ const annotations = githubSidebarAnnotations(
5365
5460
  ctx.annotationsByConversationId[conversationId] ?? []
5366
5461
  );
5367
- return annotation ? [[conversationId, [annotation]]] : [];
5462
+ return annotations.length > 0 ? [[conversationId, annotations]] : [];
5368
5463
  })
5369
5464
  )
5370
5465
  };
@@ -5451,6 +5546,7 @@ function githubPlugin(options = {}) {
5451
5546
  tools(ctx) {
5452
5547
  return createGitHubTools(ctx);
5453
5548
  },
5549
+ workspacePrepare: prepareWorkspace,
5454
5550
  async sandboxPrepare(ctx) {
5455
5551
  const hooksPath = `${ctx.sandbox.juniorRoot}/git-hooks`;
5456
5552
  await ctx.sandbox.writeFile({
@@ -0,0 +1,4 @@
1
+ /** Sandbox root directories reserved for Junior runtime material. */
2
+ export declare const RESERVED_SANDBOX_DIRECTORIES: Set<string>;
3
+ /** True when a checkout path collides with a reserved sandbox root (case-insensitive). */
4
+ export declare function isReservedSandboxDirectory(path: string): boolean;
@@ -0,0 +1,3 @@
1
+ import type { WorkspacePrepareHookContext } from "@sentry/junior-plugin-api";
2
+ /** Clone GitHub repositories into their host-selected Workspace paths. */
3
+ export declare function prepareWorkspace(ctx: WorkspacePrepareHookContext): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.161.0",
3
+ "version": "0.163.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,7 +31,7 @@
31
31
  "@sinclair/typebox": "^0.34.49",
32
32
  "drizzle-orm": "^0.45.2",
33
33
  "zod": "^4.4.3",
34
- "@sentry/junior-plugin-api": "0.161.0"
34
+ "@sentry/junior-plugin-api": "0.163.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",