@sentry/junior-github 0.108.0 → 0.110.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 +2 -2
- package/dist/db/schema.d.ts +45 -0
- package/dist/index.js +298 -174
- package/dist/issue-outcomes/store.d.ts +6 -0
- package/migrations/0004_marvelous_toad_men.sql +5 -0
- package/migrations/meta/0004_snapshot.json +275 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +2 -2
- package/skills/attach-github-assets/SOURCES.md +2 -2
- package/skills/attach-github-assets/SPEC.md +2 -2
- package/skills/github-code/SKILL.md +3 -14
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
|
|
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,
|
|
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.
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -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,37 @@ var pullRequestRepositoryStatsSchema = z10.object({
|
|
|
1665
1676
|
};
|
|
1666
1677
|
});
|
|
1667
1678
|
var issueStatsSchema = z10.object({
|
|
1668
|
-
|
|
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
|
-
var
|
|
1690
|
+
var pullRequestDaySchema = z10.object({
|
|
1678
1691
|
closed: z10.number().int().nonnegative(),
|
|
1679
1692
|
created: z10.number().int().nonnegative(),
|
|
1680
|
-
|
|
1693
|
+
date: z10.string().date(),
|
|
1694
|
+
merged: z10.number().int().nonnegative()
|
|
1695
|
+
}).strict();
|
|
1696
|
+
var issueDaySchema = z10.object({
|
|
1697
|
+
closedCompleted: z10.number().int().nonnegative(),
|
|
1698
|
+
closedDuplicate: z10.number().int().nonnegative(),
|
|
1699
|
+
closedNotPlanned: z10.number().int().nonnegative(),
|
|
1700
|
+
closedUnknown: z10.number().int().nonnegative(),
|
|
1701
|
+
created: z10.number().int().nonnegative(),
|
|
1702
|
+
date: z10.string().date()
|
|
1703
|
+
}).strict();
|
|
1704
|
+
var issueRepositoryStatsSchema = z10.object({
|
|
1705
|
+
closedCompleted: z10.number().int().nonnegative(),
|
|
1706
|
+
closedDuplicate: z10.number().int().nonnegative(),
|
|
1707
|
+
closedNotPlanned: z10.number().int().nonnegative(),
|
|
1708
|
+
closedUnknown: z10.number().int().nonnegative(),
|
|
1709
|
+
created: z10.number().int().nonnegative(),
|
|
1681
1710
|
repository: z10.string().min(1)
|
|
1682
1711
|
}).strict();
|
|
1683
1712
|
function queryRows(result) {
|
|
@@ -1707,8 +1736,7 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1707
1736
|
${table.mergedAt},
|
|
1708
1737
|
${table.closedAt}
|
|
1709
1738
|
FROM ${table}
|
|
1710
|
-
WHERE ${table.
|
|
1711
|
-
OR ${table.openedAt} >= ${oldestStart}
|
|
1739
|
+
WHERE ${table.openedAt} >= ${oldestStart}
|
|
1712
1740
|
OR ${table.mergedAt} >= ${oldestStart}
|
|
1713
1741
|
OR ${table.closedAt} >= ${oldestStart}
|
|
1714
1742
|
)
|
|
@@ -1727,8 +1755,6 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1727
1755
|
WHERE recent_pull_requests.state = 'closed_unmerged'
|
|
1728
1756
|
AND recent_pull_requests.closed_at >= windows.start_at
|
|
1729
1757
|
)::integer AS "closed",
|
|
1730
|
-
count(recent_pull_requests.pull_request_id)
|
|
1731
|
-
FILTER (WHERE recent_pull_requests.state = 'open')::integer AS "open",
|
|
1732
1758
|
count(recent_pull_requests.pull_request_id)
|
|
1733
1759
|
FILTER (
|
|
1734
1760
|
WHERE recent_pull_requests.state = 'merged'
|
|
@@ -1766,6 +1792,55 @@ async function aggregatePullRequestWindows(args) {
|
|
|
1766
1792
|
`);
|
|
1767
1793
|
return z10.array(pullRequestStatsSchema).parse(queryRows(result));
|
|
1768
1794
|
}
|
|
1795
|
+
async function aggregatePullRequestDays(args) {
|
|
1796
|
+
const end = new Date(args.nowMs);
|
|
1797
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
|
|
1798
|
+
const table = juniorGitHubPullRequests;
|
|
1799
|
+
const result = await args.db.execute(sql3`
|
|
1800
|
+
WITH days AS (
|
|
1801
|
+
SELECT generate_series(
|
|
1802
|
+
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
1803
|
+
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
1804
|
+
interval '1 day'
|
|
1805
|
+
) AS day
|
|
1806
|
+
), events AS (
|
|
1807
|
+
SELECT
|
|
1808
|
+
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
1809
|
+
'created' AS kind
|
|
1810
|
+
FROM ${table}
|
|
1811
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1812
|
+
UNION ALL
|
|
1813
|
+
SELECT
|
|
1814
|
+
date_trunc('day', ${table.mergedAt} AT TIME ZONE 'UTC') AS day,
|
|
1815
|
+
'merged' AS kind
|
|
1816
|
+
FROM ${table}
|
|
1817
|
+
WHERE ${table.state} = 'merged' AND ${table.mergedAt} >= ${start}
|
|
1818
|
+
UNION ALL
|
|
1819
|
+
SELECT
|
|
1820
|
+
date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
|
|
1821
|
+
'closed' AS kind
|
|
1822
|
+
FROM ${table}
|
|
1823
|
+
WHERE ${table.state} = 'closed_unmerged' AND ${table.closedAt} >= ${start}
|
|
1824
|
+
), daily AS (
|
|
1825
|
+
SELECT
|
|
1826
|
+
events.day,
|
|
1827
|
+
count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
|
|
1828
|
+
count(*) FILTER (WHERE events.kind = 'merged')::integer AS merged,
|
|
1829
|
+
count(*) FILTER (WHERE events.kind = 'closed')::integer AS closed
|
|
1830
|
+
FROM events
|
|
1831
|
+
GROUP BY events.day
|
|
1832
|
+
)
|
|
1833
|
+
SELECT
|
|
1834
|
+
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
1835
|
+
coalesce(daily.created, 0)::integer AS "created",
|
|
1836
|
+
coalesce(daily.merged, 0)::integer AS "merged",
|
|
1837
|
+
coalesce(daily.closed, 0)::integer AS "closed"
|
|
1838
|
+
FROM days
|
|
1839
|
+
LEFT JOIN daily ON daily.day = days.day
|
|
1840
|
+
ORDER BY days.day
|
|
1841
|
+
`);
|
|
1842
|
+
return z10.array(pullRequestDaySchema).parse(queryRows(result));
|
|
1843
|
+
}
|
|
1769
1844
|
async function aggregatePullRequestRepositories(args) {
|
|
1770
1845
|
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
1771
1846
|
const table = juniorGitHubPullRequests;
|
|
@@ -1781,7 +1856,6 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
1781
1856
|
WHERE ${table.state} = 'closed_unmerged'
|
|
1782
1857
|
AND ${table.closedAt} >= ${start}
|
|
1783
1858
|
)::integer AS "closed",
|
|
1784
|
-
count(*) FILTER (WHERE ${table.state} = 'open')::integer AS "open",
|
|
1785
1859
|
count(*) FILTER (
|
|
1786
1860
|
WHERE ${table.state} = 'merged'
|
|
1787
1861
|
AND ${table.mergedAt} >= ${start}
|
|
@@ -1798,8 +1872,7 @@ async function aggregatePullRequestRepositories(args) {
|
|
|
1798
1872
|
AND ${table.commitComposition} IS NULL
|
|
1799
1873
|
)::integer AS "compositionUnknown"
|
|
1800
1874
|
FROM ${table}
|
|
1801
|
-
WHERE ${table.
|
|
1802
|
-
OR ${table.openedAt} >= ${start}
|
|
1875
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1803
1876
|
OR ${table.mergedAt} >= ${start}
|
|
1804
1877
|
OR ${table.closedAt} >= ${start}
|
|
1805
1878
|
GROUP BY ${table.repositoryFullName}
|
|
@@ -1824,11 +1897,11 @@ async function aggregateIssueWindows(args) {
|
|
|
1824
1897
|
SELECT
|
|
1825
1898
|
${table.issueId},
|
|
1826
1899
|
${table.state},
|
|
1900
|
+
${table.stateReason},
|
|
1827
1901
|
${table.openedAt},
|
|
1828
1902
|
${table.closedAt}
|
|
1829
1903
|
FROM ${table}
|
|
1830
|
-
WHERE ${table.
|
|
1831
|
-
OR ${table.openedAt} >= ${oldestStart}
|
|
1904
|
+
WHERE ${table.openedAt} >= ${oldestStart}
|
|
1832
1905
|
OR ${table.closedAt} >= ${oldestStart}
|
|
1833
1906
|
)
|
|
1834
1907
|
SELECT
|
|
@@ -1839,10 +1912,27 @@ async function aggregateIssueWindows(args) {
|
|
|
1839
1912
|
count(recent_issues.issue_id)
|
|
1840
1913
|
FILTER (
|
|
1841
1914
|
WHERE recent_issues.state = 'closed'
|
|
1915
|
+
AND recent_issues.state_reason = 'completed'
|
|
1842
1916
|
AND recent_issues.closed_at >= windows.start_at
|
|
1843
|
-
)::integer AS "
|
|
1917
|
+
)::integer AS "closedCompleted",
|
|
1918
|
+
count(recent_issues.issue_id)
|
|
1919
|
+
FILTER (
|
|
1920
|
+
WHERE recent_issues.state = 'closed'
|
|
1921
|
+
AND recent_issues.state_reason = 'duplicate'
|
|
1922
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1923
|
+
)::integer AS "closedDuplicate",
|
|
1844
1924
|
count(recent_issues.issue_id)
|
|
1845
|
-
FILTER (
|
|
1925
|
+
FILTER (
|
|
1926
|
+
WHERE recent_issues.state = 'closed'
|
|
1927
|
+
AND recent_issues.state_reason = 'not_planned'
|
|
1928
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1929
|
+
)::integer AS "closedNotPlanned",
|
|
1930
|
+
count(recent_issues.issue_id)
|
|
1931
|
+
FILTER (
|
|
1932
|
+
WHERE recent_issues.state = 'closed'
|
|
1933
|
+
AND recent_issues.state_reason IS NULL
|
|
1934
|
+
AND recent_issues.closed_at >= windows.start_at
|
|
1935
|
+
)::integer AS "closedUnknown",
|
|
1846
1936
|
(
|
|
1847
1937
|
percentile_cont(0.5) WITHIN GROUP (
|
|
1848
1938
|
ORDER BY extract(
|
|
@@ -1860,6 +1950,57 @@ async function aggregateIssueWindows(args) {
|
|
|
1860
1950
|
`);
|
|
1861
1951
|
return z10.array(issueStatsSchema).parse(queryRows(result));
|
|
1862
1952
|
}
|
|
1953
|
+
async function aggregateIssueDays(args) {
|
|
1954
|
+
const end = new Date(args.nowMs);
|
|
1955
|
+
const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1) - 1) * DAY_MS);
|
|
1956
|
+
const table = juniorGitHubIssues;
|
|
1957
|
+
const result = await args.db.execute(sql3`
|
|
1958
|
+
WITH days AS (
|
|
1959
|
+
SELECT generate_series(
|
|
1960
|
+
date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
|
|
1961
|
+
date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
|
|
1962
|
+
interval '1 day'
|
|
1963
|
+
) AS day
|
|
1964
|
+
), events AS (
|
|
1965
|
+
SELECT
|
|
1966
|
+
date_trunc('day', ${table.openedAt} AT TIME ZONE 'UTC') AS day,
|
|
1967
|
+
'created' AS kind
|
|
1968
|
+
FROM ${table}
|
|
1969
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1970
|
+
UNION ALL
|
|
1971
|
+
SELECT
|
|
1972
|
+
date_trunc('day', ${table.closedAt} AT TIME ZONE 'UTC') AS day,
|
|
1973
|
+
coalesce(${table.stateReason}, 'unknown') AS kind
|
|
1974
|
+
FROM ${table}
|
|
1975
|
+
WHERE ${table.state} = 'closed' AND ${table.closedAt} >= ${start}
|
|
1976
|
+
), daily AS (
|
|
1977
|
+
SELECT
|
|
1978
|
+
events.day,
|
|
1979
|
+
count(*) FILTER (WHERE events.kind = 'created')::integer AS created,
|
|
1980
|
+
count(*) FILTER (WHERE events.kind = 'completed')::integer
|
|
1981
|
+
AS closed_completed,
|
|
1982
|
+
count(*) FILTER (WHERE events.kind = 'duplicate')::integer
|
|
1983
|
+
AS closed_duplicate,
|
|
1984
|
+
count(*) FILTER (WHERE events.kind = 'not_planned')::integer
|
|
1985
|
+
AS closed_not_planned,
|
|
1986
|
+
count(*) FILTER (WHERE events.kind = 'unknown')::integer
|
|
1987
|
+
AS closed_unknown
|
|
1988
|
+
FROM events
|
|
1989
|
+
GROUP BY events.day
|
|
1990
|
+
)
|
|
1991
|
+
SELECT
|
|
1992
|
+
to_char(days.day, 'YYYY-MM-DD') AS "date",
|
|
1993
|
+
coalesce(daily.created, 0)::integer AS "created",
|
|
1994
|
+
coalesce(daily.closed_completed, 0)::integer AS "closedCompleted",
|
|
1995
|
+
coalesce(daily.closed_duplicate, 0)::integer AS "closedDuplicate",
|
|
1996
|
+
coalesce(daily.closed_not_planned, 0)::integer AS "closedNotPlanned",
|
|
1997
|
+
coalesce(daily.closed_unknown, 0)::integer AS "closedUnknown"
|
|
1998
|
+
FROM days
|
|
1999
|
+
LEFT JOIN daily ON daily.day = days.day
|
|
2000
|
+
ORDER BY days.day
|
|
2001
|
+
`);
|
|
2002
|
+
return z10.array(issueDaySchema).parse(queryRows(result));
|
|
2003
|
+
}
|
|
1863
2004
|
async function aggregateIssueRepositories(args) {
|
|
1864
2005
|
const start = new Date(args.nowMs - 30 * DAY_MS);
|
|
1865
2006
|
const table = juniorGitHubIssues;
|
|
@@ -1869,15 +2010,30 @@ async function aggregateIssueRepositories(args) {
|
|
|
1869
2010
|
count(*) FILTER (WHERE ${table.openedAt} >= ${start})::integer
|
|
1870
2011
|
AS "created",
|
|
1871
2012
|
count(*) FILTER (
|
|
1872
|
-
WHERE ${table.state} = 'closed'
|
|
1873
|
-
|
|
1874
|
-
|
|
2013
|
+
WHERE ${table.state} = 'closed'
|
|
2014
|
+
AND ${table.stateReason} = 'completed'
|
|
2015
|
+
AND ${table.closedAt} >= ${start}
|
|
2016
|
+
)::integer AS "closedCompleted",
|
|
2017
|
+
count(*) FILTER (
|
|
2018
|
+
WHERE ${table.state} = 'closed'
|
|
2019
|
+
AND ${table.stateReason} = 'duplicate'
|
|
2020
|
+
AND ${table.closedAt} >= ${start}
|
|
2021
|
+
)::integer AS "closedDuplicate",
|
|
2022
|
+
count(*) FILTER (
|
|
2023
|
+
WHERE ${table.state} = 'closed'
|
|
2024
|
+
AND ${table.stateReason} = 'not_planned'
|
|
2025
|
+
AND ${table.closedAt} >= ${start}
|
|
2026
|
+
)::integer AS "closedNotPlanned",
|
|
2027
|
+
count(*) FILTER (
|
|
2028
|
+
WHERE ${table.state} = 'closed'
|
|
2029
|
+
AND ${table.stateReason} IS NULL
|
|
2030
|
+
AND ${table.closedAt} >= ${start}
|
|
2031
|
+
)::integer AS "closedUnknown"
|
|
1875
2032
|
FROM ${table}
|
|
1876
|
-
WHERE ${table.
|
|
1877
|
-
OR ${table.openedAt} >= ${start}
|
|
2033
|
+
WHERE ${table.openedAt} >= ${start}
|
|
1878
2034
|
OR ${table.closedAt} >= ${start}
|
|
1879
2035
|
GROUP BY ${table.repositoryFullName}
|
|
1880
|
-
ORDER BY "created" DESC, "
|
|
2036
|
+
ORDER BY "created" DESC, "closedCompleted" DESC, "repository" ASC
|
|
1881
2037
|
LIMIT 25
|
|
1882
2038
|
`);
|
|
1883
2039
|
return z10.array(issueRepositoryStatsSchema).parse(queryRows(result));
|
|
@@ -1895,11 +2051,25 @@ function formatDuration(value) {
|
|
|
1895
2051
|
function formatCommitComposition(stats) {
|
|
1896
2052
|
return `${stats.juniorOnly} Junior-only \xB7 ${stats.mixed} mixed \xB7 ${stats.compositionUnknown} unknown`;
|
|
1897
2053
|
}
|
|
2054
|
+
function startOfUtcDay(timestampMs) {
|
|
2055
|
+
const date = new Date(timestampMs);
|
|
2056
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
2057
|
+
return date;
|
|
2058
|
+
}
|
|
1898
2059
|
async function buildGitHubOutcomeReport(args) {
|
|
1899
|
-
const [
|
|
2060
|
+
const [
|
|
2061
|
+
windows,
|
|
2062
|
+
pullRequestDays,
|
|
2063
|
+
repositories,
|
|
2064
|
+
issueWindows,
|
|
2065
|
+
issueDays,
|
|
2066
|
+
issueRepositories
|
|
2067
|
+
] = await Promise.all([
|
|
1900
2068
|
aggregatePullRequestWindows(args),
|
|
2069
|
+
aggregatePullRequestDays(args),
|
|
1901
2070
|
aggregatePullRequestRepositories(args),
|
|
1902
2071
|
aggregateIssueWindows(args),
|
|
2072
|
+
aggregateIssueDays(args),
|
|
1903
2073
|
aggregateIssueRepositories(args)
|
|
1904
2074
|
]);
|
|
1905
2075
|
const thirtyDays = windows.find((window) => window.days === 30);
|
|
@@ -1908,17 +2078,10 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1908
2078
|
generatedAt: new Date(args.nowMs).toISOString(),
|
|
1909
2079
|
title: "GitHub work delivered",
|
|
1910
2080
|
metrics: [
|
|
1911
|
-
{ label: "PRs created \xB7 30d", value: String(thirtyDays.created) },
|
|
1912
|
-
{
|
|
1913
|
-
label: "PRs merged \xB7 30d",
|
|
1914
|
-
tone: thirtyDays.merged > 0 ? "good" : "neutral",
|
|
1915
|
-
value: String(thirtyDays.merged)
|
|
1916
|
-
},
|
|
1917
2081
|
{
|
|
1918
2082
|
label: "Junior-only merge rate \xB7 30d",
|
|
1919
2083
|
value: formatPercent(thirtyDays.juniorOnlyRate)
|
|
1920
2084
|
},
|
|
1921
|
-
{ label: "PRs open now", value: String(thirtyDays.open) },
|
|
1922
2085
|
{
|
|
1923
2086
|
label: "PR merge rate \xB7 30d",
|
|
1924
2087
|
value: formatPercent(thirtyDays.mergeRate)
|
|
@@ -1927,34 +2090,60 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1927
2090
|
label: "median PR merge time \xB7 30d",
|
|
1928
2091
|
value: formatDuration(thirtyDays.medianMergeTimeMs)
|
|
1929
2092
|
},
|
|
1930
|
-
{
|
|
1931
|
-
|
|
2093
|
+
{
|
|
2094
|
+
label: "median issue close time \xB7 30d",
|
|
2095
|
+
value: formatDuration(issueThirtyDays.medianCloseTimeMs)
|
|
2096
|
+
}
|
|
1932
2097
|
],
|
|
1933
|
-
|
|
2098
|
+
widgets: [
|
|
1934
2099
|
{
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2100
|
+
id: "pull-request-outcomes",
|
|
2101
|
+
type: "bar_chart",
|
|
2102
|
+
title: "Pull request outcomes",
|
|
2103
|
+
description: "Daily outcomes",
|
|
2104
|
+
timeRangeDays: [...WINDOWS],
|
|
2105
|
+
series: [
|
|
1938
2106
|
{ key: "created", label: "Created" },
|
|
1939
|
-
{ key: "merged", label: "Merged" },
|
|
1940
|
-
{ key: "closed", label: "Closed unmerged" }
|
|
1941
|
-
{ key: "commitComposition", label: "Merged commit composition" },
|
|
1942
|
-
{ key: "mergeRate", label: "Merge rate" },
|
|
1943
|
-
{ key: "mergeTime", label: "Median merge time" }
|
|
2107
|
+
{ key: "merged", label: "Merged", tone: "good" },
|
|
2108
|
+
{ key: "closed", label: "Closed unmerged", tone: "danger" }
|
|
1944
2109
|
],
|
|
1945
|
-
|
|
1946
|
-
id:
|
|
2110
|
+
categories: pullRequestDays.map((stats) => ({
|
|
2111
|
+
id: stats.date,
|
|
2112
|
+
label: stats.date,
|
|
1947
2113
|
values: {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
closed: String(stats.closed),
|
|
1952
|
-
commitComposition: formatCommitComposition(stats),
|
|
1953
|
-
mergeRate: formatPercent(stats.mergeRate),
|
|
1954
|
-
mergeTime: formatDuration(stats.medianMergeTimeMs)
|
|
2114
|
+
created: stats.created,
|
|
2115
|
+
merged: stats.merged,
|
|
2116
|
+
closed: stats.closed
|
|
1955
2117
|
}
|
|
1956
2118
|
}))
|
|
1957
2119
|
},
|
|
2120
|
+
{
|
|
2121
|
+
id: "issue-outcomes",
|
|
2122
|
+
type: "bar_chart",
|
|
2123
|
+
title: "Issue outcomes",
|
|
2124
|
+
description: "Daily outcomes",
|
|
2125
|
+
timeRangeDays: [...WINDOWS],
|
|
2126
|
+
series: [
|
|
2127
|
+
{ key: "created", label: "Created" },
|
|
2128
|
+
{ key: "completed", label: "Completed", tone: "good" },
|
|
2129
|
+
{ key: "duplicate", label: "Duplicate", tone: "warning" },
|
|
2130
|
+
{ key: "notPlanned", label: "Not planned", tone: "danger" },
|
|
2131
|
+
{ key: "unknown", label: "Unknown" }
|
|
2132
|
+
],
|
|
2133
|
+
categories: issueDays.map((stats) => ({
|
|
2134
|
+
id: stats.date,
|
|
2135
|
+
label: stats.date,
|
|
2136
|
+
values: {
|
|
2137
|
+
created: stats.created,
|
|
2138
|
+
completed: stats.closedCompleted,
|
|
2139
|
+
duplicate: stats.closedDuplicate,
|
|
2140
|
+
notPlanned: stats.closedNotPlanned,
|
|
2141
|
+
unknown: stats.closedUnknown
|
|
2142
|
+
}
|
|
2143
|
+
}))
|
|
2144
|
+
}
|
|
2145
|
+
],
|
|
2146
|
+
recordSets: [
|
|
1958
2147
|
{
|
|
1959
2148
|
title: "Pull request repositories \xB7 30d",
|
|
1960
2149
|
emptyText: "No Junior-owned pull request activity yet.",
|
|
@@ -1963,7 +2152,6 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1963
2152
|
{ key: "created", label: "Created" },
|
|
1964
2153
|
{ key: "merged", label: "Merged" },
|
|
1965
2154
|
{ key: "closed", label: "Closed unmerged" },
|
|
1966
|
-
{ key: "open", label: "Open now" },
|
|
1967
2155
|
{ key: "commitComposition", label: "Merged commit composition" },
|
|
1968
2156
|
{ key: "mergeRate", label: "Merge rate" }
|
|
1969
2157
|
],
|
|
@@ -1974,48 +2162,31 @@ async function buildGitHubOutcomeReport(args) {
|
|
|
1974
2162
|
created: String(stats.created),
|
|
1975
2163
|
merged: String(stats.merged),
|
|
1976
2164
|
closed: String(stats.closed),
|
|
1977
|
-
open: String(stats.open),
|
|
1978
2165
|
commitComposition: formatCommitComposition(stats),
|
|
1979
2166
|
mergeRate: formatPercent(stats.mergeRate)
|
|
1980
2167
|
}
|
|
1981
2168
|
}))
|
|
1982
2169
|
},
|
|
1983
|
-
{
|
|
1984
|
-
title: "Issue outcome windows",
|
|
1985
|
-
fields: [
|
|
1986
|
-
{ key: "window", label: "Window" },
|
|
1987
|
-
{ key: "created", label: "Created" },
|
|
1988
|
-
{ key: "closed", label: "Closed" },
|
|
1989
|
-
{ key: "open", label: "Open now" },
|
|
1990
|
-
{ key: "closeTime", label: "Median close time" }
|
|
1991
|
-
],
|
|
1992
|
-
records: issueWindows.map((stats) => ({
|
|
1993
|
-
id: `${stats.days}d`,
|
|
1994
|
-
values: {
|
|
1995
|
-
window: `${stats.days} days`,
|
|
1996
|
-
created: String(stats.created),
|
|
1997
|
-
closed: String(stats.closed),
|
|
1998
|
-
open: String(stats.open),
|
|
1999
|
-
closeTime: formatDuration(stats.medianCloseTimeMs)
|
|
2000
|
-
}
|
|
2001
|
-
}))
|
|
2002
|
-
},
|
|
2003
2170
|
{
|
|
2004
2171
|
title: "Issue repositories \xB7 30d",
|
|
2005
2172
|
emptyText: "No Junior-owned issue activity yet.",
|
|
2006
2173
|
fields: [
|
|
2007
2174
|
{ key: "repository", label: "Repository" },
|
|
2008
2175
|
{ key: "created", label: "Created" },
|
|
2009
|
-
{ key: "
|
|
2010
|
-
{ key: "
|
|
2176
|
+
{ key: "completed", label: "Completed" },
|
|
2177
|
+
{ key: "duplicate", label: "Duplicate" },
|
|
2178
|
+
{ key: "notPlanned", label: "Not planned" },
|
|
2179
|
+
{ key: "unknown", label: "Unknown reason" }
|
|
2011
2180
|
],
|
|
2012
2181
|
records: issueRepositories.map(({ repository, ...stats }) => ({
|
|
2013
2182
|
id: repository,
|
|
2014
2183
|
values: {
|
|
2015
2184
|
repository,
|
|
2016
2185
|
created: String(stats.created),
|
|
2017
|
-
|
|
2018
|
-
|
|
2186
|
+
completed: String(stats.closedCompleted),
|
|
2187
|
+
duplicate: String(stats.closedDuplicate),
|
|
2188
|
+
notPlanned: String(stats.closedNotPlanned),
|
|
2189
|
+
unknown: String(stats.closedUnknown)
|
|
2019
2190
|
}
|
|
2020
2191
|
}))
|
|
2021
2192
|
}
|
|
@@ -2254,6 +2425,52 @@ if [ -n "\${JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS:-}" ]; then
|
|
|
2254
2425
|
done <<< "$JUNIOR_GIT_ACTOR_COAUTHOR_TRAILERS"
|
|
2255
2426
|
fi
|
|
2256
2427
|
|
|
2428
|
+
# The hook owns co-author attribution. Remove every model-supplied co-author
|
|
2429
|
+
# from the final Git trailer block before appending the host-derived actors.
|
|
2430
|
+
tmp_file=$(mktemp)
|
|
2431
|
+
trap 'rm -f "$tmp_file"' EXIT
|
|
2432
|
+
awk '
|
|
2433
|
+
{ lines[NR] = $0 }
|
|
2434
|
+
END {
|
|
2435
|
+
line = NR
|
|
2436
|
+
while (line > 0 && lines[line] == "") {
|
|
2437
|
+
line--
|
|
2438
|
+
}
|
|
2439
|
+
start = line
|
|
2440
|
+
while (start > 0 && lines[start] != "") {
|
|
2441
|
+
start--
|
|
2442
|
+
}
|
|
2443
|
+
valid = line > 0
|
|
2444
|
+
for (i = start + 1; valid && i <= line; i++) {
|
|
2445
|
+
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
2446
|
+
valid = 0
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (!valid) {
|
|
2450
|
+
for (i = 1; i <= NR; i++) {
|
|
2451
|
+
print lines[i]
|
|
2452
|
+
}
|
|
2453
|
+
exit 0
|
|
2454
|
+
}
|
|
2455
|
+
kept = 0
|
|
2456
|
+
for (i = start + 1; i <= line; i++) {
|
|
2457
|
+
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
2458
|
+
kept++
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
before = kept > 0 ? start : start - 1
|
|
2462
|
+
for (i = 1; i <= before; i++) {
|
|
2463
|
+
print lines[i]
|
|
2464
|
+
}
|
|
2465
|
+
for (i = start + 1; i <= line; i++) {
|
|
2466
|
+
if (tolower(lines[i]) !~ /^co-authored-by: /) {
|
|
2467
|
+
print lines[i]
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
' "$message_file" > "$tmp_file"
|
|
2472
|
+
cat "$tmp_file" > "$message_file"
|
|
2473
|
+
|
|
2257
2474
|
final_trailer_block=$(awk '
|
|
2258
2475
|
{ lines[NR] = $0 }
|
|
2259
2476
|
END {
|
|
@@ -2279,57 +2496,6 @@ final_trailer_block=$(awk '
|
|
|
2279
2496
|
}
|
|
2280
2497
|
' "$message_file")
|
|
2281
2498
|
|
|
2282
|
-
duplicate_desired_trailer=false
|
|
2283
|
-
while IFS= read -r desired_trailer; do
|
|
2284
|
-
if [ -n "$desired_trailer" ] && [ "$(printf '%s\\n' "$final_trailer_block" | grep -Fxc -- "$desired_trailer")" -gt 1 ]; then
|
|
2285
|
-
duplicate_desired_trailer=true
|
|
2286
|
-
break
|
|
2287
|
-
fi
|
|
2288
|
-
done <<< "$desired_trailers"
|
|
2289
|
-
|
|
2290
|
-
if [ "$duplicate_desired_trailer" = true ]; then
|
|
2291
|
-
tmp_file=$(mktemp)
|
|
2292
|
-
desired_file=$(mktemp)
|
|
2293
|
-
trap 'rm -f "$tmp_file" "$desired_file"' EXIT
|
|
2294
|
-
printf '%s' "$desired_trailers" > "$desired_file"
|
|
2295
|
-
awk -v desired_file="$desired_file" '
|
|
2296
|
-
BEGIN {
|
|
2297
|
-
while ((getline value < desired_file) > 0) {
|
|
2298
|
-
if (value != "") {
|
|
2299
|
-
wanted[value] = 1
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
close(desired_file)
|
|
2303
|
-
}
|
|
2304
|
-
{ lines[NR] = $0 }
|
|
2305
|
-
END {
|
|
2306
|
-
line = NR
|
|
2307
|
-
while (line > 0 && lines[line] == "") {
|
|
2308
|
-
line--
|
|
2309
|
-
}
|
|
2310
|
-
start = line
|
|
2311
|
-
while (start > 0 && lines[start] != "") {
|
|
2312
|
-
start--
|
|
2313
|
-
}
|
|
2314
|
-
valid = line > 0
|
|
2315
|
-
for (i = start + 1; valid && i <= line; i++) {
|
|
2316
|
-
if (lines[i] !~ /^[[:alnum:]-]+: .+/) {
|
|
2317
|
-
valid = 0
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
for (i = 1; i <= NR; i++) {
|
|
2321
|
-
if (valid && i > start && i <= line && wanted[lines[i]]) {
|
|
2322
|
-
if (seen[lines[i]]++) {
|
|
2323
|
-
continue
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
print lines[i]
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
' "$message_file" > "$tmp_file"
|
|
2330
|
-
cat "$tmp_file" > "$message_file"
|
|
2331
|
-
fi
|
|
2332
|
-
|
|
2333
2499
|
missing_trailers=""
|
|
2334
2500
|
collect_missing_trailer() {
|
|
2335
2501
|
if ! printf '%s\\n' "$final_trailer_block" | grep -Fqx -- "$1"; then
|
|
@@ -2641,28 +2807,6 @@ function githubRepositoryFromLeaseScope(leaseScope) {
|
|
|
2641
2807
|
}
|
|
2642
2808
|
return { owner: match[1], name: match[2] };
|
|
2643
2809
|
}
|
|
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
2810
|
async function resolveUserAccount(tokens) {
|
|
2667
2811
|
const account = await githubRequest("https://api.github.com", "/user", {
|
|
2668
2812
|
token: tokens.accessToken
|
|
@@ -2843,8 +2987,7 @@ async function issueInstallationToken(options) {
|
|
|
2843
2987
|
const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
|
|
2844
2988
|
const body = {
|
|
2845
2989
|
...permissions ? { permissions } : {},
|
|
2846
|
-
..."repositories" in options ? { repositories: options.repositories } : {}
|
|
2847
|
-
..."repositoryIds" in options ? { repository_ids: options.repositoryIds } : {}
|
|
2990
|
+
..."repositories" in options ? { repositories: options.repositories } : {}
|
|
2848
2991
|
};
|
|
2849
2992
|
const accessTokenResponse = await githubRequest(
|
|
2850
2993
|
"https://api.github.com",
|
|
@@ -2864,7 +3007,6 @@ async function issueInstallationToken(options) {
|
|
|
2864
3007
|
async function issueInstallationCredential(options) {
|
|
2865
3008
|
const token = await issueInstallationToken(options);
|
|
2866
3009
|
return createCredentialLease({
|
|
2867
|
-
...options.domains ? { domains: options.domains } : {},
|
|
2868
3010
|
token: token.token,
|
|
2869
3011
|
expiresAtMs: token.expiresAtMs
|
|
2870
3012
|
});
|
|
@@ -3183,13 +3325,7 @@ async function githubGrantForEgress(ctx) {
|
|
|
3183
3325
|
upstreamUrl
|
|
3184
3326
|
});
|
|
3185
3327
|
if (isGitHubAssetUploadRequest(method, upstreamUrl)) {
|
|
3186
|
-
|
|
3187
|
-
return grantForAccess(
|
|
3188
|
-
"write",
|
|
3189
|
-
"github.asset-upload",
|
|
3190
|
-
"installation-write",
|
|
3191
|
-
githubRepositoryIdLeaseScope(repositoryId)
|
|
3192
|
-
);
|
|
3328
|
+
return grantForAccess("write", "github.asset-upload", "user-write");
|
|
3193
3329
|
}
|
|
3194
3330
|
const smartHttpAccess = githubSmartHttpAccess(upstreamUrl);
|
|
3195
3331
|
if (smartHttpAccess) {
|
|
@@ -3423,18 +3559,6 @@ function githubPlugin(options = {}) {
|
|
|
3423
3559
|
});
|
|
3424
3560
|
}
|
|
3425
3561
|
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
3562
|
const repository = githubRepositoryFromLeaseScope(
|
|
3439
3563
|
ctx.grant.leaseScope
|
|
3440
3564
|
);
|
|
@@ -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,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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.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.
|
|
34
|
+
"@sentry/junior-plugin-api": "0.110.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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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.
|