@sentry/junior-github 0.108.0 → 0.109.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/SETUP.md CHANGED
@@ -133,8 +133,8 @@ Use `additionalUserScopes` only when a human-identity integration flow requires
133
133
  ## 3) Runtime behavior
134
134
 
135
135
  - When either GitHub skill is active, authenticated `gh` and `git` commands cause the runtime to inject GitHub credentials automatically for the current turn.
136
- - The plugin classifies GitHub traffic from the forwarded HTTP request. Reads use `installation-read`, while `GET /user` uses `user-read`. Allowlisted App-owned mutations, Git smart-HTTP pushes, and user-attachment uploads to `uploads.github.com/user-attachments/assets` use repository-scoped `installation-write`. Unknown REST writes and GraphQL mutations are denied.
137
- - `user-read` and explicitly human `user-write` operations require the actor, or an explicitly delegated user subject, to authorize the GitHub App through the private OAuth flow. Junior-owned issue, pull request, branch, and asset-upload operations do not fall back to user OAuth.
136
+ - The plugin classifies GitHub traffic from the forwarded HTTP request. Reads use `installation-read`, while `GET /user` uses `user-read`. Allowlisted App-owned mutations and Git smart-HTTP pushes use repository-scoped `installation-write`. User-attachment uploads to `uploads.github.com/user-attachments/assets` use `user-write`. Unknown REST writes and GraphQL mutations are denied.
137
+ - `user-read` and explicitly human `user-write` operations require the actor, or an explicitly delegated user subject, to authorize the GitHub App through the private OAuth flow. Junior-owned issue, pull request, and branch operations do not fall back to user OAuth.
138
138
  - Headless resource-event turns use the `resource-event` system actor and may receive the same repository-scoped installation grants. This lets Junior respond to subscribed pull request events by committing and pushing fixes without inheriting a subscriber's OAuth credential.
139
139
  - Git commits use Junior as author and committer. Resolvable human run actors are credited once with `Co-Authored-By` trailers.
140
140
  - Issued credentials are reused only within the current turn, credential leases are cached by plugin grant and repository lease scope, and upstream 403 permission denials clear the cached lease before the next retry.
@@ -15,6 +15,13 @@ export declare const githubIssueStateSchema: z.ZodEnum<{
15
15
  closed: "closed";
16
16
  }>;
17
17
  export type GitHubIssueState = z.output<typeof githubIssueStateSchema>;
18
+ export declare const githubIssueStateReasonSchema: z.ZodEnum<{
19
+ completed: "completed";
20
+ duplicate: "duplicate";
21
+ not_planned: "not_planned";
22
+ reopened: "reopened";
23
+ }>;
24
+ export type GitHubIssueStateReason = z.output<typeof githubIssueStateReasonSchema>;
18
25
  /** Current provider projection for issues classified as Junior-owned. */
19
26
  export declare const juniorGitHubIssues: import("drizzle-orm/pg-core").PgTableWithColumns<{
20
27
  name: "junior_github_issues";
@@ -107,6 +114,25 @@ export declare const juniorGitHubIssues: import("drizzle-orm/pg-core").PgTableWi
107
114
  }, {}, {
108
115
  $type: "open" | "closed";
109
116
  }>;
117
+ stateReason: import("drizzle-orm/pg-core").PgColumn<{
118
+ name: "state_reason";
119
+ tableName: "junior_github_issues";
120
+ dataType: "string";
121
+ columnType: "PgText";
122
+ data: "completed" | "duplicate" | "not_planned" | "reopened";
123
+ driverParam: string;
124
+ notNull: false;
125
+ hasDefault: false;
126
+ isPrimaryKey: false;
127
+ isAutoincrement: false;
128
+ hasRuntimeDefault: false;
129
+ enumValues: [string, ...string[]];
130
+ baseColumn: never;
131
+ identity: undefined;
132
+ generated: undefined;
133
+ }, {}, {
134
+ $type: "completed" | "duplicate" | "not_planned" | "reopened";
135
+ }>;
110
136
  openedAt: import("drizzle-orm/pg-core").PgColumn<{
111
137
  name: "opened_at";
112
138
  tableName: "junior_github_issues";
@@ -478,6 +504,25 @@ export declare const githubSqlSchema: {
478
504
  }, {}, {
479
505
  $type: "open" | "closed";
480
506
  }>;
507
+ stateReason: import("drizzle-orm/pg-core").PgColumn<{
508
+ name: "state_reason";
509
+ tableName: "junior_github_issues";
510
+ dataType: "string";
511
+ columnType: "PgText";
512
+ data: "completed" | "duplicate" | "not_planned" | "reopened";
513
+ driverParam: string;
514
+ notNull: false;
515
+ hasDefault: false;
516
+ isPrimaryKey: false;
517
+ isAutoincrement: false;
518
+ hasRuntimeDefault: false;
519
+ enumValues: [string, ...string[]];
520
+ baseColumn: never;
521
+ identity: undefined;
522
+ generated: undefined;
523
+ }, {}, {
524
+ $type: "completed" | "duplicate" | "not_planned" | "reopened";
525
+ }>;
481
526
  openedAt: import("drizzle-orm/pg-core").PgColumn<{
482
527
  name: "opened_at";
483
528
  tableName: "junior_github_issues";
package/dist/index.js CHANGED
@@ -1113,6 +1113,12 @@ var githubPullRequestCommitCompositionSchema = z5.enum([
1113
1113
  "mixed"
1114
1114
  ]);
1115
1115
  var githubIssueStateSchema = z5.enum(["closed", "open"]);
1116
+ var githubIssueStateReasonSchema = z5.enum([
1117
+ "completed",
1118
+ "duplicate",
1119
+ "not_planned",
1120
+ "reopened"
1121
+ ]);
1116
1122
  var juniorGitHubIssues = pgTable(
1117
1123
  "junior_github_issues",
1118
1124
  {
@@ -1121,6 +1127,7 @@ var juniorGitHubIssues = pgTable(
1121
1127
  repositoryFullName: text("repository_full_name").notNull(),
1122
1128
  number: integer("number").notNull(),
1123
1129
  state: text("state").$type().notNull(),
1130
+ stateReason: text("state_reason").$type(),
1124
1131
  openedAt: timestamp("opened_at", { withTimezone: true }).notNull(),
1125
1132
  closedAt: timestamp("closed_at", { withTimezone: true }),
1126
1133
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
@@ -1164,6 +1171,7 @@ var githubIssueOutcomeInputSchema = z6.object({
1164
1171
  repositoryFullName: z6.string().min(1),
1165
1172
  repositoryId: z6.string().min(1),
1166
1173
  state: githubIssueStateSchema,
1174
+ stateReason: githubIssueStateReasonSchema.optional(),
1167
1175
  updatedAt: z6.date()
1168
1176
  }).strict();
1169
1177
  function projectionValues(input) {
@@ -1174,6 +1182,7 @@ function projectionValues(input) {
1174
1182
  repositoryFullName: input.repositoryFullName,
1175
1183
  repositoryId: input.repositoryId,
1176
1184
  state: input.state,
1185
+ stateReason: input.stateReason ?? null,
1177
1186
  updatedAt: input.updatedAt
1178
1187
  };
1179
1188
  }
@@ -1305,6 +1314,7 @@ var canonicalIssueOutcomeSchema = z8.object({
1305
1314
  created_at: z8.string(),
1306
1315
  id: z8.number().int().positive(),
1307
1316
  number: z8.number().int().positive(),
1317
+ state_reason: githubIssueStateReasonSchema.nullable().optional(),
1308
1318
  updated_at: z8.string(),
1309
1319
  user: z8.object({ login: z8.string().min(1) }).strict()
1310
1320
  }).strict(),
@@ -1321,6 +1331,7 @@ var issueOutcomeSchema = z8.object({
1321
1331
  created_at: z8.string(),
1322
1332
  id: z8.number().int().positive(),
1323
1333
  number: z8.number().int().positive(),
1334
+ state_reason: githubIssueStateReasonSchema.nullable().optional(),
1324
1335
  updated_at: z8.string(),
1325
1336
  user: z8.object({ login: z8.string().min(1) }).passthrough()
1326
1337
  }).passthrough(),
@@ -1337,6 +1348,7 @@ var issueOutcomeSchema = z8.object({
1337
1348
  created_at: provider.issue.created_at,
1338
1349
  id: provider.issue.id,
1339
1350
  number: provider.issue.number,
1351
+ state_reason: provider.issue.state_reason,
1340
1352
  updated_at: provider.issue.updated_at,
1341
1353
  user: { login: provider.issue.user.login }
1342
1354
  },
@@ -1388,6 +1400,7 @@ function normalizeGitHubIssueOutcome(args) {
1388
1400
  repositoryFullName: parsed.repository.full_name,
1389
1401
  repositoryId: String(parsed.repository.id),
1390
1402
  state: parsed.action === "closed" ? "closed" : "open",
1403
+ stateReason: parsed.action === "closed" ? issue.state_reason ?? void 0 : void 0,
1391
1404
  updatedAt
1392
1405
  };
1393
1406
  }
@@ -1636,8 +1649,7 @@ var pullRequestStatsSchema = z10.object({
1636
1649
  juniorOnly: z10.number().int().nonnegative(),
1637
1650
  medianMergeTimeMs: z10.number().nonnegative().nullable(),
1638
1651
  merged: z10.number().int().nonnegative(),
1639
- mixed: z10.number().int().nonnegative(),
1640
- open: z10.number().int().nonnegative()
1652
+ mixed: z10.number().int().nonnegative()
1641
1653
  }).strict().transform((row) => {
1642
1654
  const terminal = row.merged + row.closed;
1643
1655
  const classified = row.juniorOnly + row.mixed;
@@ -1655,7 +1667,6 @@ var pullRequestRepositoryStatsSchema = z10.object({
1655
1667
  juniorOnly: z10.number().int().nonnegative(),
1656
1668
  merged: z10.number().int().nonnegative(),
1657
1669
  mixed: z10.number().int().nonnegative(),
1658
- open: z10.number().int().nonnegative(),
1659
1670
  repository: z10.string().min(1)
1660
1671
  }).strict().transform((row) => {
1661
1672
  const terminal = row.merged + row.closed;
@@ -1665,19 +1676,23 @@ var pullRequestRepositoryStatsSchema = z10.object({
1665
1676
  };
1666
1677
  });
1667
1678
  var issueStatsSchema = z10.object({
1668
- closed: z10.number().int().nonnegative(),
1679
+ closedCompleted: z10.number().int().nonnegative(),
1680
+ closedDuplicate: z10.number().int().nonnegative(),
1681
+ closedNotPlanned: z10.number().int().nonnegative(),
1682
+ closedUnknown: z10.number().int().nonnegative(),
1669
1683
  created: z10.number().int().nonnegative(),
1670
1684
  days: z10.number().int().positive(),
1671
- medianCloseTimeMs: z10.number().nonnegative().nullable(),
1672
- open: z10.number().int().nonnegative()
1685
+ medianCloseTimeMs: z10.number().nonnegative().nullable()
1673
1686
  }).strict().transform((row) => ({
1674
1687
  ...row,
1675
1688
  medianCloseTimeMs: row.medianCloseTimeMs ?? void 0
1676
1689
  }));
1677
1690
  var issueRepositoryStatsSchema = z10.object({
1678
- closed: z10.number().int().nonnegative(),
1691
+ closedCompleted: z10.number().int().nonnegative(),
1692
+ closedDuplicate: z10.number().int().nonnegative(),
1693
+ closedNotPlanned: z10.number().int().nonnegative(),
1694
+ closedUnknown: z10.number().int().nonnegative(),
1679
1695
  created: z10.number().int().nonnegative(),
1680
- open: z10.number().int().nonnegative(),
1681
1696
  repository: z10.string().min(1)
1682
1697
  }).strict();
1683
1698
  function queryRows(result) {
@@ -1707,8 +1722,7 @@ async function aggregatePullRequestWindows(args) {
1707
1722
  ${table.mergedAt},
1708
1723
  ${table.closedAt}
1709
1724
  FROM ${table}
1710
- WHERE ${table.state} = 'open'
1711
- OR ${table.openedAt} >= ${oldestStart}
1725
+ WHERE ${table.openedAt} >= ${oldestStart}
1712
1726
  OR ${table.mergedAt} >= ${oldestStart}
1713
1727
  OR ${table.closedAt} >= ${oldestStart}
1714
1728
  )
@@ -1727,8 +1741,6 @@ async function aggregatePullRequestWindows(args) {
1727
1741
  WHERE recent_pull_requests.state = 'closed_unmerged'
1728
1742
  AND recent_pull_requests.closed_at >= windows.start_at
1729
1743
  )::integer AS "closed",
1730
- count(recent_pull_requests.pull_request_id)
1731
- FILTER (WHERE recent_pull_requests.state = 'open')::integer AS "open",
1732
1744
  count(recent_pull_requests.pull_request_id)
1733
1745
  FILTER (
1734
1746
  WHERE recent_pull_requests.state = 'merged'
@@ -1781,7 +1793,6 @@ async function aggregatePullRequestRepositories(args) {
1781
1793
  WHERE ${table.state} = 'closed_unmerged'
1782
1794
  AND ${table.closedAt} >= ${start}
1783
1795
  )::integer AS "closed",
1784
- count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open",
1785
1796
  count(*) FILTER (
1786
1797
  WHERE ${table.state} = 'merged'
1787
1798
  AND ${table.mergedAt} >= ${start}
@@ -1798,8 +1809,7 @@ async function aggregatePullRequestRepositories(args) {
1798
1809
  AND ${table.commitComposition} IS NULL
1799
1810
  )::integer AS "compositionUnknown"
1800
1811
  FROM ${table}
1801
- WHERE ${table.state} = 'open'
1802
- OR ${table.openedAt} >= ${start}
1812
+ WHERE ${table.openedAt} >= ${start}
1803
1813
  OR ${table.mergedAt} >= ${start}
1804
1814
  OR ${table.closedAt} >= ${start}
1805
1815
  GROUP BY ${table.repositoryFullName}
@@ -1824,11 +1834,11 @@ async function aggregateIssueWindows(args) {
1824
1834
  SELECT
1825
1835
  ${table.issueId},
1826
1836
  ${table.state},
1837
+ ${table.stateReason},
1827
1838
  ${table.openedAt},
1828
1839
  ${table.closedAt}
1829
1840
  FROM ${table}
1830
- WHERE ${table.state} = 'open'
1831
- OR ${table.openedAt} >= ${oldestStart}
1841
+ WHERE ${table.openedAt} >= ${oldestStart}
1832
1842
  OR ${table.closedAt} >= ${oldestStart}
1833
1843
  )
1834
1844
  SELECT
@@ -1839,10 +1849,27 @@ async function aggregateIssueWindows(args) {
1839
1849
  count(recent_issues.issue_id)
1840
1850
  FILTER (
1841
1851
  WHERE recent_issues.state = 'closed'
1852
+ AND recent_issues.state_reason = 'completed'
1842
1853
  AND recent_issues.closed_at >= windows.start_at
1843
- )::integer AS "closed",
1854
+ )::integer AS "closedCompleted",
1855
+ count(recent_issues.issue_id)
1856
+ FILTER (
1857
+ WHERE recent_issues.state = 'closed'
1858
+ AND recent_issues.state_reason = 'duplicate'
1859
+ AND recent_issues.closed_at >= windows.start_at
1860
+ )::integer AS "closedDuplicate",
1861
+ count(recent_issues.issue_id)
1862
+ FILTER (
1863
+ WHERE recent_issues.state = 'closed'
1864
+ AND recent_issues.state_reason = 'not_planned'
1865
+ AND recent_issues.closed_at >= windows.start_at
1866
+ )::integer AS "closedNotPlanned",
1844
1867
  count(recent_issues.issue_id)
1845
- FILTER (WHERE recent_issues.state = 'open')::integer AS "open",
1868
+ FILTER (
1869
+ WHERE recent_issues.state = 'closed'
1870
+ AND recent_issues.state_reason IS NULL
1871
+ AND recent_issues.closed_at >= windows.start_at
1872
+ )::integer AS "closedUnknown",
1846
1873
  (
1847
1874
  percentile_cont(0.5) WITHIN GROUP (
1848
1875
  ORDER BY extract(
@@ -1869,15 +1896,30 @@ async function aggregateIssueRepositories(args) {
1869
1896
  count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
1870
1897
  AS "created",
1871
1898
  count(*) FILTER (
1872
- WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
1873
- )::integer AS "closed",
1874
- count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open"
1899
+ WHERE ${table.state} = 'closed'
1900
+ AND ${table.stateReason} = 'completed'
1901
+ AND ${table.closedAt} >= ${start}
1902
+ )::integer AS "closedCompleted",
1903
+ count(*) FILTER (
1904
+ WHERE ${table.state} = 'closed'
1905
+ AND ${table.stateReason} = 'duplicate'
1906
+ AND ${table.closedAt} >= ${start}
1907
+ )::integer AS "closedDuplicate",
1908
+ count(*) FILTER (
1909
+ WHERE ${table.state} = 'closed'
1910
+ AND ${table.stateReason} = 'not_planned'
1911
+ AND ${table.closedAt} >= ${start}
1912
+ )::integer AS "closedNotPlanned",
1913
+ count(*) FILTER (
1914
+ WHERE ${table.state} = 'closed'
1915
+ AND ${table.stateReason} IS NULL
1916
+ AND ${table.closedAt} >= ${start}
1917
+ )::integer AS "closedUnknown"
1875
1918
  FROM ${table}
1876
- WHERE ${table.state} = 'open'
1877
- OR ${table.openedAt} >= ${start}
1919
+ WHERE ${table.openedAt} >= ${start}
1878
1920
  OR ${table.closedAt} >= ${start}
1879
1921
  GROUP BY ${table.repositoryFullName}
1880
- ORDER BY "created" DESC, "closed" DESC, "repository" ASC
1922
+ ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
1881
1923
  LIMIT 25
1882
1924
  `);
1883
1925
  return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
@@ -1918,7 +1960,10 @@ async function buildGitHubOutcomeReport(args) {
1918
1960
  label: "Junior-only merge rate \xB7 30d",
1919
1961
  value: formatPercent(thirtyDays.juniorOnlyRate)
1920
1962
  },
1921
- { label: "PRs open now", value: String(thirtyDays.open) },
1963
+ {
1964
+ label: "PRs closed unmerged \xB7 30d",
1965
+ value: String(thirtyDays.closed)
1966
+ },
1922
1967
  {
1923
1968
  label: "PR merge rate \xB7 30d",
1924
1969
  value: formatPercent(thirtyDays.mergeRate)
@@ -1928,7 +1973,19 @@ async function buildGitHubOutcomeReport(args) {
1928
1973
  value: formatDuration(thirtyDays.medianMergeTimeMs)
1929
1974
  },
1930
1975
  { label: "issues created \xB7 30d", value: String(issueThirtyDays.created) },
1931
- { label: "issues open now", value: String(issueThirtyDays.open) }
1976
+ {
1977
+ label: "issues completed \xB7 30d",
1978
+ tone: issueThirtyDays.closedCompleted > 0 ? "good" : "neutral",
1979
+ value: String(issueThirtyDays.closedCompleted)
1980
+ },
1981
+ {
1982
+ label: "issues closed as duplicate \xB7 30d",
1983
+ value: String(issueThirtyDays.closedDuplicate)
1984
+ },
1985
+ {
1986
+ label: "issues closed as not planned \xB7 30d",
1987
+ value: String(issueThirtyDays.closedNotPlanned)
1988
+ }
1932
1989
  ],
1933
1990
  recordSets: [
1934
1991
  {
@@ -1963,7 +2020,6 @@ async function buildGitHubOutcomeReport(args) {
1963
2020
  { key: "created", label: "Created" },
1964
2021
  { key: "merged", label: "Merged" },
1965
2022
  { key: "closed", label: "Closed unmerged" },
1966
- { key: "open", label: "Open now" },
1967
2023
  { key: "commitComposition", label: "Merged commit composition" },
1968
2024
  { key: "mergeRate", label: "Merge rate" }
1969
2025
  ],
@@ -1974,7 +2030,6 @@ async function buildGitHubOutcomeReport(args) {
1974
2030
  created: String(stats.created),
1975
2031
  merged: String(stats.merged),
1976
2032
  closed: String(stats.closed),
1977
- open: String(stats.open),
1978
2033
  commitComposition: formatCommitComposition(stats),
1979
2034
  mergeRate: formatPercent(stats.mergeRate)
1980
2035
  }
@@ -1985,8 +2040,10 @@ async function buildGitHubOutcomeReport(args) {
1985
2040
  fields: [
1986
2041
  { key: "window", label: "Window" },
1987
2042
  { key: "created", label: "Created" },
1988
- { key: "closed", label: "Closed" },
1989
- { key: "open", label: "Open now" },
2043
+ { key: "completed", label: "Completed" },
2044
+ { key: "duplicate", label: "Duplicate" },
2045
+ { key: "notPlanned", label: "Not planned" },
2046
+ { key: "unknown", label: "Unknown reason" },
1990
2047
  { key: "closeTime", label: "Median close time" }
1991
2048
  ],
1992
2049
  records: issueWindows.map((stats) => ({
@@ -1994,8 +2051,10 @@ async function buildGitHubOutcomeReport(args) {
1994
2051
  values: {
1995
2052
  window: `${stats.days} days`,
1996
2053
  created: String(stats.created),
1997
- closed: String(stats.closed),
1998
- open: String(stats.open),
2054
+ completed: String(stats.closedCompleted),
2055
+ duplicate: String(stats.closedDuplicate),
2056
+ notPlanned: String(stats.closedNotPlanned),
2057
+ unknown: String(stats.closedUnknown),
1999
2058
  closeTime: formatDuration(stats.medianCloseTimeMs)
2000
2059
  }
2001
2060
  }))
@@ -2006,16 +2065,20 @@ async function buildGitHubOutcomeReport(args) {
2006
2065
  fields: [
2007
2066
  { key: "repository", label: "Repository" },
2008
2067
  { key: "created", label: "Created" },
2009
- { key: "closed", label: "Closed" },
2010
- { key: "open", label: "Open now" }
2068
+ { key: "completed", label: "Completed" },
2069
+ { key: "duplicate", label: "Duplicate" },
2070
+ { key: "notPlanned", label: "Not planned" },
2071
+ { key: "unknown", label: "Unknown reason" }
2011
2072
  ],
2012
2073
  records: issueRepositories.map(({ repository, ...stats }) => ({
2013
2074
  id: repository,
2014
2075
  values: {
2015
2076
  repository,
2016
2077
  created: String(stats.created),
2017
- closed: String(stats.closed),
2018
- open: String(stats.open)
2078
+ completed: String(stats.closedCompleted),
2079
+ duplicate: String(stats.closedDuplicate),
2080
+ notPlanned: String(stats.closedNotPlanned),
2081
+ unknown: String(stats.closedUnknown)
2019
2082
  }
2020
2083
  }))
2021
2084
  }
@@ -2641,28 +2704,6 @@ function githubRepositoryFromLeaseScope(leaseScope) {
2641
2704
  }
2642
2705
  return { owner: match[1], name: match[2] };
2643
2706
  }
2644
- function githubRepositoryIdFromUploadUrl(upstreamUrl) {
2645
- const repositoryId = Number(upstreamUrl.searchParams.get("repository_id"));
2646
- if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
2647
- throw new EgressPolicyDenied(
2648
- "GitHub asset upload request is missing a valid repository_id."
2649
- );
2650
- }
2651
- return repositoryId;
2652
- }
2653
- function githubRepositoryIdLeaseScope(repositoryId) {
2654
- return `repository-id:${repositoryId}`;
2655
- }
2656
- function githubRepositoryIdFromLeaseScope(leaseScope) {
2657
- const match = /^repository-id:(\d+)$/.exec(leaseScope ?? "");
2658
- const repositoryId = Number(match?.[1]);
2659
- if (!Number.isSafeInteger(repositoryId) || repositoryId <= 0) {
2660
- throw new GitHubPluginSetupError(
2661
- "GitHub asset upload grant is missing a repository id lease scope."
2662
- );
2663
- }
2664
- return repositoryId;
2665
- }
2666
2707
  async function resolveUserAccount(tokens) {
2667
2708
  const account = await githubRequest("https://api.github.com", "/user", {
2668
2709
  token: tokens.accessToken
@@ -2843,8 +2884,7 @@ async function issueInstallationToken(options) {
2843
2884
  const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
2844
2885
  const body = {
2845
2886
  ...permissions ? { permissions } : {},
2846
- ..."repositories" in options ? { repositories: options.repositories } : {},
2847
- ..."repositoryIds" in options ? { repository_ids: options.repositoryIds } : {}
2887
+ ..."repositories" in options ? { repositories: options.repositories } : {}
2848
2888
  };
2849
2889
  const accessTokenResponse = await githubRequest(
2850
2890
  "https://api.github.com",
@@ -2864,7 +2904,6 @@ async function issueInstallationToken(options) {
2864
2904
  async function issueInstallationCredential(options) {
2865
2905
  const token = await issueInstallationToken(options);
2866
2906
  return createCredentialLease({
2867
- ...options.domains ? { domains: options.domains } : {},
2868
2907
  token: token.token,
2869
2908
  expiresAtMs: token.expiresAtMs
2870
2909
  });
@@ -3183,13 +3222,7 @@ async function githubGrantForEgress(ctx) {
3183
3222
  upstreamUrl
3184
3223
  });
3185
3224
  if (isGitHubAssetUploadRequest(method, upstreamUrl)) {
3186
- const repositoryId = githubRepositoryIdFromUploadUrl(upstreamUrl);
3187
- return grantForAccess(
3188
- "write",
3189
- "github.asset-upload",
3190
- "installation-write",
3191
- githubRepositoryIdLeaseScope(repositoryId)
3192
- );
3225
+ return grantForAccess("write", "github.asset-upload", "user-write");
3193
3226
  }
3194
3227
  const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
3195
3228
  if (smartHttpAccess) {
@@ -3423,18 +3456,6 @@ function githubPlugin(options = {}) {
3423
3456
  });
3424
3457
  }
3425
3458
  if (ctx.grant.name === "installation-write") {
3426
- if (ctx.grant.reason === "github.asset-upload") {
3427
- const repositoryId = githubRepositoryIdFromLeaseScope(
3428
- ctx.grant.leaseScope
3429
- );
3430
- return await issueInstallationCredential({
3431
- appIdEnv,
3432
- domains: GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS,
3433
- privateKeyEnv,
3434
- installationIdEnv,
3435
- repositoryIds: [repositoryId]
3436
- });
3437
- }
3438
3459
  const repository = githubRepositoryFromLeaseScope(
3439
3460
  ctx.grant.leaseScope
3440
3461
  );
@@ -12,6 +12,12 @@ declare const githubIssueOutcomeInputSchema: z.ZodObject<{
12
12
  open: "open";
13
13
  closed: "closed";
14
14
  }>;
15
+ stateReason: z.ZodOptional<z.ZodEnum<{
16
+ completed: "completed";
17
+ duplicate: "duplicate";
18
+ not_planned: "not_planned";
19
+ reopened: "reopened";
20
+ }>>;
15
21
  updatedAt: z.ZodDate;
16
22
  }, z.core.$strict>;
17
23
  export type GitHubIssueOutcomeInput = z.output<typeof githubIssueOutcomeInputSchema>;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE "junior_github_issues" ADD COLUMN "state_reason" text;
2
+ --> statement-breakpoint
3
+ UPDATE "junior_github_issues"
4
+ SET "state_reason" = 'completed'
5
+ WHERE "state" = 'closed';
@@ -0,0 +1,275 @@
1
+ {
2
+ "id": "6e0acdc7-db48-4ebf-846e-0589fc32afb5",
3
+ "prevId": "fb7f4132-d92f-4127-be54-23f93c79034b",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.junior_github_issues": {
8
+ "name": "junior_github_issues",
9
+ "schema": "",
10
+ "columns": {
11
+ "issue_id": {
12
+ "name": "issue_id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "repository_id": {
18
+ "name": "repository_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "repository_full_name": {
24
+ "name": "repository_full_name",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "number": {
30
+ "name": "number",
31
+ "type": "integer",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "state": {
36
+ "name": "state",
37
+ "type": "text",
38
+ "primaryKey": false,
39
+ "notNull": true
40
+ },
41
+ "state_reason": {
42
+ "name": "state_reason",
43
+ "type": "text",
44
+ "primaryKey": false,
45
+ "notNull": false
46
+ },
47
+ "opened_at": {
48
+ "name": "opened_at",
49
+ "type": "timestamp with time zone",
50
+ "primaryKey": false,
51
+ "notNull": true
52
+ },
53
+ "closed_at": {
54
+ "name": "closed_at",
55
+ "type": "timestamp with time zone",
56
+ "primaryKey": false,
57
+ "notNull": false
58
+ },
59
+ "updated_at": {
60
+ "name": "updated_at",
61
+ "type": "timestamp with time zone",
62
+ "primaryKey": false,
63
+ "notNull": true
64
+ }
65
+ },
66
+ "indexes": {
67
+ "junior_github_issues_opened_at_idx": {
68
+ "name": "junior_github_issues_opened_at_idx",
69
+ "columns": [
70
+ {
71
+ "expression": "opened_at",
72
+ "isExpression": false,
73
+ "asc": true,
74
+ "nulls": "last"
75
+ }
76
+ ],
77
+ "isUnique": false,
78
+ "concurrently": false,
79
+ "method": "btree",
80
+ "with": {}
81
+ },
82
+ "junior_github_issues_closed_at_idx": {
83
+ "name": "junior_github_issues_closed_at_idx",
84
+ "columns": [
85
+ {
86
+ "expression": "closed_at",
87
+ "isExpression": false,
88
+ "asc": true,
89
+ "nulls": "last"
90
+ }
91
+ ],
92
+ "isUnique": false,
93
+ "concurrently": false,
94
+ "method": "btree",
95
+ "with": {}
96
+ },
97
+ "junior_github_issues_open_idx": {
98
+ "name": "junior_github_issues_open_idx",
99
+ "columns": [
100
+ {
101
+ "expression": "issue_id",
102
+ "isExpression": false,
103
+ "asc": true,
104
+ "nulls": "last"
105
+ }
106
+ ],
107
+ "isUnique": false,
108
+ "where": "\"junior_github_issues\".\"state\" = 'open'",
109
+ "concurrently": false,
110
+ "method": "btree",
111
+ "with": {}
112
+ }
113
+ },
114
+ "foreignKeys": {},
115
+ "compositePrimaryKeys": {},
116
+ "uniqueConstraints": {},
117
+ "policies": {},
118
+ "checkConstraints": {},
119
+ "isRLSEnabled": false
120
+ },
121
+ "public.junior_github_pull_requests": {
122
+ "name": "junior_github_pull_requests",
123
+ "schema": "",
124
+ "columns": {
125
+ "pull_request_id": {
126
+ "name": "pull_request_id",
127
+ "type": "text",
128
+ "primaryKey": true,
129
+ "notNull": true
130
+ },
131
+ "repository_id": {
132
+ "name": "repository_id",
133
+ "type": "text",
134
+ "primaryKey": false,
135
+ "notNull": true
136
+ },
137
+ "repository_full_name": {
138
+ "name": "repository_full_name",
139
+ "type": "text",
140
+ "primaryKey": false,
141
+ "notNull": true
142
+ },
143
+ "number": {
144
+ "name": "number",
145
+ "type": "integer",
146
+ "primaryKey": false,
147
+ "notNull": true
148
+ },
149
+ "state": {
150
+ "name": "state",
151
+ "type": "text",
152
+ "primaryKey": false,
153
+ "notNull": true
154
+ },
155
+ "commit_composition": {
156
+ "name": "commit_composition",
157
+ "type": "text",
158
+ "primaryKey": false,
159
+ "notNull": false
160
+ },
161
+ "conversation_ids": {
162
+ "name": "conversation_ids",
163
+ "type": "text[]",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "default": "ARRAY[]::text[]"
167
+ },
168
+ "opened_at": {
169
+ "name": "opened_at",
170
+ "type": "timestamp with time zone",
171
+ "primaryKey": false,
172
+ "notNull": true
173
+ },
174
+ "merged_at": {
175
+ "name": "merged_at",
176
+ "type": "timestamp with time zone",
177
+ "primaryKey": false,
178
+ "notNull": false
179
+ },
180
+ "closed_at": {
181
+ "name": "closed_at",
182
+ "type": "timestamp with time zone",
183
+ "primaryKey": false,
184
+ "notNull": false
185
+ },
186
+ "updated_at": {
187
+ "name": "updated_at",
188
+ "type": "timestamp with time zone",
189
+ "primaryKey": false,
190
+ "notNull": true
191
+ }
192
+ },
193
+ "indexes": {
194
+ "junior_github_pull_requests_opened_at_idx": {
195
+ "name": "junior_github_pull_requests_opened_at_idx",
196
+ "columns": [
197
+ {
198
+ "expression": "opened_at",
199
+ "isExpression": false,
200
+ "asc": true,
201
+ "nulls": "last"
202
+ }
203
+ ],
204
+ "isUnique": false,
205
+ "concurrently": false,
206
+ "method": "btree",
207
+ "with": {}
208
+ },
209
+ "junior_github_pull_requests_merged_at_idx": {
210
+ "name": "junior_github_pull_requests_merged_at_idx",
211
+ "columns": [
212
+ {
213
+ "expression": "merged_at",
214
+ "isExpression": false,
215
+ "asc": true,
216
+ "nulls": "last"
217
+ }
218
+ ],
219
+ "isUnique": false,
220
+ "concurrently": false,
221
+ "method": "btree",
222
+ "with": {}
223
+ },
224
+ "junior_github_pull_requests_closed_at_idx": {
225
+ "name": "junior_github_pull_requests_closed_at_idx",
226
+ "columns": [
227
+ {
228
+ "expression": "closed_at",
229
+ "isExpression": false,
230
+ "asc": true,
231
+ "nulls": "last"
232
+ }
233
+ ],
234
+ "isUnique": false,
235
+ "concurrently": false,
236
+ "method": "btree",
237
+ "with": {}
238
+ },
239
+ "junior_github_pull_requests_open_idx": {
240
+ "name": "junior_github_pull_requests_open_idx",
241
+ "columns": [
242
+ {
243
+ "expression": "pull_request_id",
244
+ "isExpression": false,
245
+ "asc": true,
246
+ "nulls": "last"
247
+ }
248
+ ],
249
+ "isUnique": false,
250
+ "where": "\"junior_github_pull_requests\".\"state\" = 'open'",
251
+ "concurrently": false,
252
+ "method": "btree",
253
+ "with": {}
254
+ }
255
+ },
256
+ "foreignKeys": {},
257
+ "compositePrimaryKeys": {},
258
+ "uniqueConstraints": {},
259
+ "policies": {},
260
+ "checkConstraints": {},
261
+ "isRLSEnabled": false
262
+ }
263
+ },
264
+ "enums": {},
265
+ "schemas": {},
266
+ "sequences": {},
267
+ "roles": {},
268
+ "policies": {},
269
+ "views": {},
270
+ "_meta": {
271
+ "columns": {},
272
+ "schemas": {},
273
+ "tables": {}
274
+ }
275
+ }
@@ -29,6 +29,13 @@
29
29
  "when": 1784762053015,
30
30
  "tag": "0003_pull_request_conversations",
31
31
  "breakpoints": true
32
+ },
33
+ {
34
+ "idx": 4,
35
+ "version": "7",
36
+ "when": 1784774242129,
37
+ "tag": "0004_marvelous_toad_men",
38
+ "breakpoints": true
32
39
  }
33
40
  ]
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.108.0",
3
+ "version": "0.109.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.108.0"
34
+ "@sentry/junior-plugin-api": "0.109.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -13,10 +13,10 @@
13
13
 
14
14
  - **Adopted:** preserve the upstream one-file-per-upload behavior, supported formats, markdown output, and upload endpoint.
15
15
  - **Adopted:** preserve the upstream MIT license in the skill directory as explicitly required.
16
- - **Replaced:** direct `gh auth token` access with Junior's host-managed, repository-scoped installation credential injection so credentials never enter script output or files.
16
+ - **Replaced:** direct `gh auth token` access with Junior's host-managed requesting-user OAuth credential injection so credentials never enter script output or files.
17
17
  - **Replaced:** numeric repository-id override with explicit `owner/repo`; the script resolves the id through authenticated `gh api`.
18
18
  - **Narrowed:** automatic activation requires a concrete local path and GitHub destination.
19
- - **Deferred:** live upload verification because it would create an external GitHub asset; deterministic mocked validation is sufficient for implementation checks.
19
+ - **Verified:** a live upload with the repository-scoped installation credential returned HTTP 500; the endpoint's upstream implementation uses a user token.
20
20
 
21
21
  ## Coverage
22
22
 
@@ -23,7 +23,7 @@ Out of scope:
23
23
  - Run `scripts/upload.sh` once per local file.
24
24
  - Return image markdown or a bare video URL.
25
25
  - Surface per-file failures exactly enough for the user to act.
26
- - Use the GitHub plugin's repository-scoped installation credential injection; do not retrieve tokens in the script.
26
+ - Use the GitHub plugin's requesting-user OAuth credential injection; do not retrieve tokens in the script.
27
27
 
28
28
  ## Reference Architecture
29
29
 
@@ -40,7 +40,7 @@ Out of scope:
40
40
  ## Known Limitations
41
41
 
42
42
  - GitHub's user-attachment endpoint is not part of the documented public REST API.
43
- - A real upload requires GitHub to accept the repository-scoped installation credential for the target repository.
43
+ - A real upload requires the requesting user to authorize the GitHub App.
44
44
 
45
45
  ## Maintenance Notes
46
46
 
@@ -21,7 +21,8 @@ Use `git` and `gh` for repository work. Use `github_createPullRequest`, not `gh
21
21
  - Read applicable `AGENTS.md` files before editing. Narrower repo/task instructions win.
22
22
  - Preserve unrelated work. Never force-push, delete refs, or perform destructive merges.
23
23
  - Base conclusions on repository evidence. Do not claim a check ran unless it did.
24
- - Supported App-owned writes use an automatically injected repository-scoped credential. Try the operation before requesting remediation; ask for no user token.
24
+ - For Junior-owned pull requests, push the branch before creating the PR. The runtime supplies repository-scoped GitHub App credentials for both; try the operations before requesting remediation and never ask for a user token.
25
+ - A tool-routing denial requires the named tool; only an upstream denial justifies permission remediation.
25
26
  - Stop for ambiguous targets, missing access, destructive operations, or unresolved upstream permission failures.
26
27
 
27
28
  ## Workflow
@@ -36,9 +37,7 @@ For edits, choose the smallest credible validation path before changing files. C
36
37
 
37
38
  ### 2. Investigate
38
39
 
39
- Establish where the behavior lives, current versus requested behavior, root cause or gap, and the smallest proof of correctness. Read linked issues, PRs, specs, and failing output when provided. If the request is investigation-only, report evidence without editing.
40
-
41
- For non-trivial architecture, API, security, concurrency, migration, or broad cross-file work, use the available advisor after gathering evidence and before editing. Resolve material concerns in the plan.
40
+ Establish where the behavior lives, current versus requested behavior, root cause or gap, and the smallest proof of correctness. Read linked issues, PRs, specs, and failing output when provided. For pull requests, inspect conversation comments, inline review comments, reviews, the diff, and checks. If the request is investigation-only, report evidence without editing.
42
41
 
43
42
  ### 3. Edit
44
43
 
@@ -56,8 +55,6 @@ Do not install or repair the GitHub plugin runtime itself; that is manifest-owne
56
55
 
57
56
  Run targeted changed-file/package checks before broad suites. Separate regressions from baseline failures. For instruction-only changes, run available structural checks and perform a content-consistency review.
58
57
 
59
- For non-trivial work, review the final diff and initial results with the available advisor before packaging. Address material correctness, regression, and testing concerns, then rerun affected checks.
60
-
61
58
  ### 5. Package every completed edit
62
59
 
63
60
  Unless the user explicitly says not to create a PR, every completed repository edit must end in a pushed branch and PR. Default to draft; honor an explicit user or repo instruction to open it ready for review. Do not stop at local changes or a commit.
@@ -77,11 +74,3 @@ If PR creation is blocked, report the exact failed command/tool call and leave t
77
74
  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.
78
75
 
79
76
  Return: repo, branch, PR URL/number, checks and results, pre-existing failures, and anything not run with the reason.
80
-
81
- ## Operation notes
82
-
83
- - **Clone/history:** clone shallowly by default; deepen before any operation that relies on omitted ancestry.
84
- - **PR inspection:** read conversation comments, inline review comments, reviews, diff, and checks.
85
- - **PR mutation:** push before create; use only supported endpoints in the API reference.
86
- - **Workflow dispatch:** `gh workflow run` is supported only for workflows declaring `workflow_dispatch`.
87
- - **Permissions:** a tool-routing denial requires the named tool; only an upstream denial justifies permission remediation.