@sentry/junior-github 0.158.0 → 0.160.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,7 +20,8 @@ import {
20
20
  // src/plugin.ts
21
21
  import {
22
22
  defineJuniorPlugin,
23
- EgressPolicyDenied as EgressPolicyDenied2
23
+ EgressPolicyDenied as EgressPolicyDenied2,
24
+ enforceEgressPolicy
24
25
  } from "@sentry/junior-plugin-api";
25
26
 
26
27
  // src/permissions.ts
@@ -2694,19 +2695,30 @@ async function recordGitHubIssueOutcome(db, input) {
2694
2695
  const outcome = githubIssueOutcomeInputSchema.parse(input);
2695
2696
  const values = projectionValues(outcome);
2696
2697
  if (!outcome.candidateOwned) {
2697
- await db.update(juniorGitHubIssues).set(values).where(
2698
+ const updated = await db.update(juniorGitHubIssues).set(values).where(
2698
2699
  and(
2699
2700
  eq(juniorGitHubIssues.issueId, outcome.issueId),
2700
2701
  lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
2701
2702
  )
2702
- );
2703
- return;
2703
+ ).returning({
2704
+ conversationIds: juniorGitHubIssues.conversationIds
2705
+ });
2706
+ return {
2707
+ applied: updated.length > 0,
2708
+ conversationIds: updated[0]?.conversationIds ?? []
2709
+ };
2704
2710
  }
2705
- await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
2711
+ const inserted = await db.insert(juniorGitHubIssues).values({ issueId: outcome.issueId, ...values }).onConflictDoUpdate({
2706
2712
  target: juniorGitHubIssues.issueId,
2707
2713
  set: values,
2708
2714
  where: lte(juniorGitHubIssues.updatedAt, outcome.updatedAt)
2715
+ }).returning({
2716
+ conversationIds: juniorGitHubIssues.conversationIds
2709
2717
  });
2718
+ return {
2719
+ applied: inserted.length > 0,
2720
+ conversationIds: inserted[0]?.conversationIds ?? []
2721
+ };
2710
2722
  }
2711
2723
  async function recordGitHubIssueConversations(db, input) {
2712
2724
  const association = githubIssueConversationsInputSchema.parse(input);
@@ -3341,7 +3353,23 @@ function createGitHubWebhookRoute(args) {
3341
3353
  }
3342
3354
  }
3343
3355
  if (issueOutcome) {
3344
- await recordGitHubIssueOutcome(args.db, issueOutcome);
3356
+ const recordedOutcome = await recordGitHubIssueOutcome(
3357
+ args.db,
3358
+ issueOutcome
3359
+ );
3360
+ if (recordedOutcome.applied && issueOutcome.state === "closed") {
3361
+ await Promise.all(
3362
+ recordedOutcome.conversationIds.map(
3363
+ (conversationId) => args.annotations.forConversation(conversationId).upsert({
3364
+ kind: "resource_link",
3365
+ key: `${issueOutcome.repositoryFullName.toLowerCase()}#${issueOutcome.number}`,
3366
+ label: `${issueOutcome.repositoryFullName}#${issueOutcome.number}`,
3367
+ url: `https://github.com/${issueOutcome.repositoryFullName}/issues/${issueOutcome.number}`,
3368
+ status: "closed"
3369
+ })
3370
+ )
3371
+ );
3372
+ }
3345
3373
  }
3346
3374
  const recordedIssueConversations = issueConversations ? await recordGitHubIssueConversations(args.db, issueConversations) : false;
3347
3375
  const recordedPullRequestConversations = pullRequestConversations ? await recordGitHubPullRequestConversations(
@@ -4638,6 +4666,36 @@ function shouldInspectGitHubGraphqlResponse(ctx) {
4638
4666
  const contentType = ctx.response.headers.get("content-type");
4639
4667
  return contentType ? /\bjson\b/i.test(contentType) : false;
4640
4668
  }
4669
+ var GITHUB_BODY_WRITES = [
4670
+ {
4671
+ denialMessage: `GitHub issue creation must use the github_createIssue tool so Junior can own idempotency and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
4672
+ graphqlField: "createIssue",
4673
+ method: "POST",
4674
+ operation: "github.issue.create",
4675
+ restPath: /^\/repos\/[^/]+\/[^/]+\/issues$/
4676
+ },
4677
+ {
4678
+ denialMessage: `GitHub pull request creation must use the github_createPullRequest tool so Junior can own idempotency and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
4679
+ graphqlField: "createPullRequest",
4680
+ method: "POST",
4681
+ operation: "github.pull.create",
4682
+ restPath: /^\/repos\/[^/]+\/[^/]+\/pulls$/
4683
+ },
4684
+ {
4685
+ denialMessage: `GitHub issue updates must use the github_updateIssue tool so Junior can own requester attribution and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
4686
+ graphqlField: "updateIssue",
4687
+ method: "PATCH",
4688
+ operation: "github.issue.update",
4689
+ restPath: /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+$/
4690
+ },
4691
+ {
4692
+ denialMessage: `GitHub pull request updates must use the github_updatePullRequest tool so Junior can own requester attribution and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`,
4693
+ graphqlField: "updatePullRequest",
4694
+ method: "PATCH",
4695
+ operation: "github.pull.update",
4696
+ restPath: /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+$/
4697
+ }
4698
+ ];
4641
4699
  function githubApiWriteGrantName(method, upstreamUrl) {
4642
4700
  const pathname = upstreamUrl.pathname.toLowerCase();
4643
4701
  if (!isGitHubApiUrl2(upstreamUrl)) {
@@ -4653,26 +4711,19 @@ function githubApiWriteGrantName(method, upstreamUrl) {
4653
4711
  ) || /^\/repos\/[^/]+\/[^/]+\/actions\/jobs\/[^/]+\/rerun$/.test(pathname))) {
4654
4712
  return "installation-write";
4655
4713
  }
4656
- if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(pathname)) {
4714
+ if (GITHUB_BODY_WRITES.some(
4715
+ (write) => write.method === method && write.restPath.test(pathname)
4716
+ )) {
4657
4717
  return "installation-write";
4658
4718
  }
4659
4719
  if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+\/comments$/.test(pathname)) {
4660
4720
  return "installation-write";
4661
4721
  }
4662
- if (method === "PATCH" && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+$/.test(pathname)) {
4663
- return "installation-write";
4664
- }
4665
4722
  if ((method === "POST" || method === "DELETE") && /^\/repos\/[^/]+\/[^/]+\/issues\/[^/]+\/(labels|assignees)(?:\/[^/]+)?$/.test(
4666
4723
  pathname
4667
4724
  )) {
4668
4725
  return "installation-write";
4669
4726
  }
4670
- if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(pathname)) {
4671
- return "installation-write";
4672
- }
4673
- if (method === "PATCH" && /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+$/.test(pathname)) {
4674
- return "installation-write";
4675
- }
4676
4727
  if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/ready_for_review$/.test(pathname)) {
4677
4728
  return "installation-write";
4678
4729
  }
@@ -4694,17 +4745,6 @@ function githubApiWriteGrantName(method, upstreamUrl) {
4694
4745
  }
4695
4746
  return void 0;
4696
4747
  }
4697
- function isGitHubIssueCreateRestRequest(method, upstreamUrl) {
4698
- return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/issues$/.test(upstreamUrl.pathname.toLowerCase());
4699
- }
4700
- function isGitHubPullCreateRestRequest(method, upstreamUrl) {
4701
- return method === "POST" && isGitHubApiUrl2(upstreamUrl) && /^\/repos\/[^/]+\/[^/]+\/pulls$/.test(upstreamUrl.pathname.toLowerCase());
4702
- }
4703
- function isGitHubResourceUpdateRestRequest(method, upstreamUrl, resource) {
4704
- return method === "PATCH" && isGitHubApiUrl2(upstreamUrl) && new RegExp(`^/repos/[^/]+/[^/]+/${resource}/[^/]+$`).test(
4705
- upstreamUrl.pathname.toLowerCase()
4706
- );
4707
- }
4708
4748
  function isGitHubGraphqlMutation(method, upstreamUrl, bodyText, field) {
4709
4749
  if (method !== "POST" || !isGitHubGraphqlUrl(upstreamUrl)) return false;
4710
4750
  const parsed = parseGitHubGraphqlRequest(bodyText);
@@ -4716,64 +4756,26 @@ function isGitHubGraphqlMutation(method, upstreamUrl, bodyText, field) {
4716
4756
  `\\bmutation\\s+${escapeRegExp2(parsed.operationName)}\\b`
4717
4757
  ).test(parsed.normalized);
4718
4758
  }
4719
- function assertGitHubWriteAllowed(input) {
4759
+ function applyGitHubEgressPolicy(input) {
4720
4760
  assertGitHubPullRequestApprovalDenied({
4721
4761
  ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
4722
4762
  method: input.method,
4723
4763
  upstreamUrl: input.upstreamUrl
4724
4764
  });
4725
- if (input.operation === "github.issue.create") return;
4726
- if (input.operation === "github.issue.update") return;
4727
- if (input.operation === "github.pull.create") return;
4728
- if (input.operation === "github.pull.update") return;
4729
- if (isGitHubIssueCreateRestRequest(input.method, input.upstreamUrl) || isGitHubGraphqlMutation(
4730
- input.method,
4731
- input.upstreamUrl,
4732
- input.bodyText,
4733
- "createIssue"
4734
- )) {
4735
- throw new EgressPolicyDenied2(
4736
- `GitHub issue creation must use the github_createIssue tool so Junior can own idempotency and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`
4737
- );
4738
- }
4739
- if (isGitHubPullCreateRestRequest(input.method, input.upstreamUrl) || isGitHubGraphqlMutation(
4740
- input.method,
4741
- input.upstreamUrl,
4742
- input.bodyText,
4743
- "createPullRequest"
4744
- )) {
4745
- throw new EgressPolicyDenied2(
4746
- `GitHub pull request creation must use the github_createPullRequest tool so Junior can own idempotency and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`
4747
- );
4748
- }
4749
- if (isGitHubResourceUpdateRestRequest(
4750
- input.method,
4751
- input.upstreamUrl,
4752
- "issues"
4753
- ) || isGitHubGraphqlMutation(
4754
- input.method,
4755
- input.upstreamUrl,
4756
- input.bodyText,
4757
- "updateIssue"
4758
- )) {
4759
- throw new EgressPolicyDenied2(
4760
- `GitHub issue updates must use the github_updateIssue tool so Junior can own requester attribution and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`
4761
- );
4762
- }
4763
- if (isGitHubResourceUpdateRestRequest(
4764
- input.method,
4765
- input.upstreamUrl,
4766
- "pulls"
4767
- ) || isGitHubGraphqlMutation(
4768
- input.method,
4769
- input.upstreamUrl,
4770
- input.bodyText,
4771
- "updatePullRequest"
4772
- )) {
4773
- throw new EgressPolicyDenied2(
4774
- `GitHub pull request updates must use the github_updatePullRequest tool so Junior can own requester attribution and the conversation footer. ${CREATE_TOOL_ROUTING_GUIDANCE}`
4775
- );
4776
- }
4765
+ const pathname = input.upstreamUrl.pathname.toLowerCase();
4766
+ const write = GITHUB_BODY_WRITES.find(
4767
+ (candidate) => candidate.method === input.method && isGitHubApiUrl2(input.upstreamUrl) && candidate.restPath.test(pathname) || isGitHubGraphqlMutation(
4768
+ input.method,
4769
+ input.upstreamUrl,
4770
+ input.bodyText,
4771
+ candidate.graphqlField
4772
+ )
4773
+ );
4774
+ if (!write) return;
4775
+ enforceEgressPolicy({
4776
+ allowed: input.operation === write.operation,
4777
+ denialMessage: write.denialMessage
4778
+ });
4777
4779
  }
4778
4780
  function grantForAccess(access, reason, name, leaseScope) {
4779
4781
  return {
@@ -4796,7 +4798,7 @@ function repositoryLeaseScope(upstreamUrl) {
4796
4798
  async function githubGrantForEgress(ctx) {
4797
4799
  const method = ctx.request.method.toUpperCase();
4798
4800
  const upstreamUrl = new URL(ctx.request.url);
4799
- assertGitHubWriteAllowed({
4801
+ applyGitHubEgressPolicy({
4800
4802
  ...ctx.request.bodyText !== void 0 ? { bodyText: ctx.request.bodyText } : {},
4801
4803
  method,
4802
4804
  ...ctx.request.operation ? { operation: ctx.request.operation } : {},
@@ -30,8 +30,13 @@ export type GitHubIssueConversationsInput = z.output<typeof githubIssueConversat
30
30
  * Record the newest lifecycle projection for one Junior-owned issue.
31
31
  * Ownership-qualified opening or closing events insert; later events update
32
32
  * existing rows, and older provider timestamps cannot regress them.
33
+ * Returns the written row state so follow-up annotation updates stay scoped to
34
+ * associated conversations.
33
35
  */
34
- export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<void>;
36
+ export declare function recordGitHubIssueOutcome(db: GitHubDb, input: GitHubIssueOutcomeInput): Promise<{
37
+ applied: boolean;
38
+ conversationIds: string[];
39
+ }>;
35
40
  /** Append native conversation ids to an existing Junior-owned issue projection. */
36
41
  export declare function recordGitHubIssueConversations(db: GitHubDb, input: GitHubIssueConversationsInput): Promise<boolean>;
37
42
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.158.0",
3
+ "version": "0.160.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.158.0"
34
+ "@sentry/junior-plugin-api": "0.160.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -81,3 +81,5 @@ If PR creation or update is blocked, report the exact failed command/tool call a
81
81
  When PR creation returns a subscribable resource hint, subscribe to suggested review/CI events. Report only actionable feedback addressed, build failures fixed, fully green/ready state, or merge.
82
82
 
83
83
  Return: repo, branch, PR URL/number, checks and results, pre-existing failures, and anything not run with the reason.
84
+
85
+ When you mention a pull request in a user-facing reply, always include a direct link. Prefer the full PR URL or a Markdown link such as `[#123](https://github.com/owner/repo/pull/123)`. `owner/repo#number` is also fine. Do not leave bare `PR #123` text without a URL or repo.