@sentry/junior-github 0.181.1 → 0.183.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/chunk-KXAUXJ3Q.js +1808 -0
- package/dist/egress-policy.d.ts +11 -0
- package/dist/index.js +158 -763
- package/dist/plugin.d.ts +1 -1
- package/dist/pull-request-outcomes/store.d.ts +1 -0
- package/dist/resource-events/pull-request.d.ts +7 -1
- package/dist/testing.js +1 -1
- package/dist/webhooks/check-suite.d.ts +80 -0
- package/dist/webhooks/handler.d.ts +5 -3
- package/dist/webhooks/resource-events.d.ts +5 -37
- package/migrations/0008_native_code_backfill.sql +74 -0
- package/migrations/meta/0008_snapshot.json +348 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +2 -2
- package/skills/github-code/SKILL.md +2 -2
- package/skills/github-code/SPEC.md +1 -1
- package/dist/chunk-GXZVNEIB.js +0 -825
- package/dist/webhooks/check-suite-enrichment.d.ts +0 -11
package/dist/index.js
CHANGED
|
@@ -1,728 +1,56 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CREATE_TOOL_ROUTING_GUIDANCE,
|
|
3
|
+
GITHUB_APP_ID_ENV,
|
|
4
|
+
GITHUB_APP_PRIVATE_KEY_ENV,
|
|
5
|
+
GITHUB_AUTH_TOKEN_ENV,
|
|
6
|
+
GITHUB_AUTH_TOKEN_PLACEHOLDER,
|
|
2
7
|
GITHUB_DEPLOYMENT_EVENTS,
|
|
3
8
|
GITHUB_DEPLOYMENT_SUGGESTED_EVENTS,
|
|
9
|
+
GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES,
|
|
10
|
+
GITHUB_INSTALLATION_ID_ENV,
|
|
4
11
|
GITHUB_ISSUE_EVENTS,
|
|
5
12
|
GITHUB_ISSUE_SUGGESTED_EVENTS,
|
|
6
13
|
GITHUB_PULL_REQUEST_EVENTS,
|
|
14
|
+
GITHUB_PULL_REQUEST_MATCH_FIELDS,
|
|
7
15
|
GITHUB_PULL_REQUEST_SUGGESTED_EVENTS,
|
|
8
16
|
GITHUB_RELEASE_EVENTS,
|
|
9
17
|
GITHUB_RELEASE_SUGGESTED_EVENTS,
|
|
18
|
+
GitHubPluginSetupError,
|
|
19
|
+
HTTP_READ_METHODS,
|
|
20
|
+
USER_TOKEN_GRANTS,
|
|
21
|
+
USER_WRITE_REQUIREMENTS,
|
|
22
|
+
createPermissionCache,
|
|
23
|
+
credentialUnavailable,
|
|
10
24
|
gitHubDeploymentSourceSubscribable,
|
|
11
25
|
gitHubIssueSubscribable,
|
|
12
26
|
gitHubPullRequestSubscribable,
|
|
13
27
|
gitHubReleaseSourceSubscribable,
|
|
14
28
|
gitHubRepositorySubscribable,
|
|
29
|
+
githubRepositoryFromLeaseScope,
|
|
30
|
+
githubRepositoryFromUrl,
|
|
31
|
+
githubRepositoryLeaseScope,
|
|
32
|
+
githubRequest,
|
|
33
|
+
isRecord,
|
|
34
|
+
issueInstallationCredential,
|
|
35
|
+
issueInstallationToken,
|
|
36
|
+
issueUserCredential,
|
|
37
|
+
loadCheckSuiteFacts,
|
|
38
|
+
needsCheckSuitePullRequestFacts,
|
|
15
39
|
normalizeGitHubResourceEvents,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
normalizePermissions,
|
|
41
|
+
normalizeScopeList,
|
|
42
|
+
parseCheckSuitePublishTargets,
|
|
43
|
+
readEnv,
|
|
44
|
+
readGrantPermissions,
|
|
45
|
+
requireEnv,
|
|
46
|
+
resolveUserAccount
|
|
47
|
+
} from "./chunk-KXAUXJ3Q.js";
|
|
19
48
|
|
|
20
49
|
// src/plugin.ts
|
|
21
50
|
import {
|
|
22
|
-
defineJuniorPlugin
|
|
23
|
-
EgressPolicyDenied as EgressPolicyDenied2,
|
|
24
|
-
enforceEgressPolicy
|
|
51
|
+
defineJuniorPlugin
|
|
25
52
|
} from "@sentry/junior-plugin-api";
|
|
26
53
|
|
|
27
|
-
// src/permissions.ts
|
|
28
|
-
var LEVELS = /* @__PURE__ */ new Set(["read", "write", "admin"]);
|
|
29
|
-
var WRITE_ONLY_PERMISSIONS = /* @__PURE__ */ new Set(["profile", "workflows"]);
|
|
30
|
-
function isLevel(value) {
|
|
31
|
-
return LEVELS.has(value);
|
|
32
|
-
}
|
|
33
|
-
function normalizeScope(rawScope) {
|
|
34
|
-
return String(rawScope).trim().replace(/-/g, "_");
|
|
35
|
-
}
|
|
36
|
-
function normalizePermissions(permissions) {
|
|
37
|
-
if (permissions === void 0) {
|
|
38
|
-
return void 0;
|
|
39
|
-
}
|
|
40
|
-
const entries = Object.entries(permissions);
|
|
41
|
-
if (entries.length === 0) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
"githubPlugin appPermissions must contain at least one permission when provided."
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
const request = {};
|
|
47
|
-
for (const [rawScope, rawLevel] of entries) {
|
|
48
|
-
const normalizedScope = normalizeScope(rawScope);
|
|
49
|
-
if (!normalizedScope) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
"githubPlugin appPermissions contains an empty permission name."
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
if (!/^[a-z][a-z0-9_]*$/.test(normalizedScope)) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`githubPlugin appPermissions contains invalid permission "${rawScope}".`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
if (!isLevel(rawLevel)) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
`githubPlugin appPermissions.${rawScope} must be "read", "write", or "admin".`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
request[normalizedScope] = rawLevel;
|
|
65
|
-
}
|
|
66
|
-
return request;
|
|
67
|
-
}
|
|
68
|
-
function readGrantPermissions(permissions) {
|
|
69
|
-
const readOnly = { metadata: "read" };
|
|
70
|
-
for (const [scope, level] of Object.entries(permissions ?? {})) {
|
|
71
|
-
if (!isLevel(level)) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`GitHub permission "${scope}" returned invalid level "${String(level)}".`
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (!WRITE_ONLY_PERMISSIONS.has(scope)) {
|
|
77
|
-
readOnly[scope] = "read";
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return readOnly;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// src/pull-request-review-policy.ts
|
|
84
|
-
import { EgressPolicyDenied } from "@sentry/junior-plugin-api";
|
|
85
|
-
|
|
86
|
-
// src/credential-support.ts
|
|
87
|
-
import { createPrivateKey, createSign } from "crypto";
|
|
88
|
-
var GITHUB_APP_ID_ENV = "GITHUB_APP_ID";
|
|
89
|
-
var GITHUB_APP_PRIVATE_KEY_ENV = "GITHUB_APP_PRIVATE_KEY";
|
|
90
|
-
var GITHUB_INSTALLATION_ID_ENV = "GITHUB_INSTALLATION_ID";
|
|
91
|
-
var GITHUB_AUTH_TOKEN_ENV = "GITHUB_TOKEN";
|
|
92
|
-
var GITHUB_AUTH_TOKEN_PLACEHOLDER = "ghp_host_managed_credential";
|
|
93
|
-
var MAX_LEASE_MS = 60 * 60 * 1e3;
|
|
94
|
-
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
95
|
-
var USER_REFRESH_TIMEOUT_MS = 2e4;
|
|
96
|
-
var GITHUB_GRAPHQL_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
|
|
97
|
-
var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
98
|
-
var USER_TOKEN_GRANTS = /* @__PURE__ */ new Set(["user-read", "user-write"]);
|
|
99
|
-
var CREATE_TOOL_ROUTING_GUIDANCE = "This is a Junior tool-routing denial, not a GitHub permission failure. Do not ask the user for GitHub permissions; retry with the required Junior tool.";
|
|
100
|
-
var USER_WRITE_REQUIREMENTS = [
|
|
101
|
-
"requesting GitHub user permission to perform this operation"
|
|
102
|
-
];
|
|
103
|
-
var GITHUB_CREDENTIAL_DOMAINS = ["api.github.com", "github.com"];
|
|
104
|
-
var GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS = [
|
|
105
|
-
...GITHUB_CREDENTIAL_DOMAINS,
|
|
106
|
-
"uploads.github.com"
|
|
107
|
-
];
|
|
108
|
-
var GitHubUserRefreshRejectedError = class extends Error {
|
|
109
|
-
constructor(message) {
|
|
110
|
-
super(message);
|
|
111
|
-
this.name = "GitHubUserRefreshRejectedError";
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
var GitHubRequestError = class extends Error {
|
|
115
|
-
status;
|
|
116
|
-
constructor(message, status) {
|
|
117
|
-
super(message);
|
|
118
|
-
this.name = "GitHubRequestError";
|
|
119
|
-
this.status = status;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
var GitHubPluginSetupError = class extends Error {
|
|
123
|
-
constructor(message) {
|
|
124
|
-
super(message);
|
|
125
|
-
this.name = "GitHubPluginSetupError";
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
function isRecord(value) {
|
|
129
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
130
|
-
}
|
|
131
|
-
function readEnv(name) {
|
|
132
|
-
const value = process.env[name];
|
|
133
|
-
if (typeof value !== "string") {
|
|
134
|
-
return void 0;
|
|
135
|
-
}
|
|
136
|
-
const trimmed = value.trim();
|
|
137
|
-
return trimmed ? trimmed : void 0;
|
|
138
|
-
}
|
|
139
|
-
function requireEnv(name) {
|
|
140
|
-
const value = readEnv(name);
|
|
141
|
-
if (!value) {
|
|
142
|
-
throw new GitHubPluginSetupError(`Missing ${name}`);
|
|
143
|
-
}
|
|
144
|
-
return value;
|
|
145
|
-
}
|
|
146
|
-
function normalizeScopeList(scopes) {
|
|
147
|
-
return [
|
|
148
|
-
...new Set(
|
|
149
|
-
(scopes ?? []).flatMap((scope) => String(scope).split(/\s+/)).map((scope) => scope.trim()).filter(Boolean)
|
|
150
|
-
)
|
|
151
|
-
].sort();
|
|
152
|
-
}
|
|
153
|
-
function normalizeOAuthScope(scope) {
|
|
154
|
-
const normalized = normalizeScopeList(scope ? [scope] : []);
|
|
155
|
-
return normalized.length ? normalized.join(" ") : void 0;
|
|
156
|
-
}
|
|
157
|
-
function hasRequiredOAuthScope(storedScope, requiredScope) {
|
|
158
|
-
const required = normalizeScopeList(requiredScope ? [requiredScope] : []);
|
|
159
|
-
if (required.length === 0) {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
const stored = new Set(normalizeScopeList(storedScope ? [storedScope] : []));
|
|
163
|
-
if (stored.size === 0) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
return required.every((scope) => stored.has(scope));
|
|
167
|
-
}
|
|
168
|
-
function isGitHubApiUrl(upstreamUrl) {
|
|
169
|
-
return upstreamUrl.hostname.toLowerCase() === "api.github.com";
|
|
170
|
-
}
|
|
171
|
-
function base64Url(input) {
|
|
172
|
-
return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
173
|
-
}
|
|
174
|
-
function getPrivateKey(envName) {
|
|
175
|
-
const raw = requireEnv(envName);
|
|
176
|
-
let key;
|
|
177
|
-
try {
|
|
178
|
-
key = createPrivateKey({ key: raw, format: "pem" });
|
|
179
|
-
} catch {
|
|
180
|
-
throw new GitHubPluginSetupError(
|
|
181
|
-
`Invalid ${envName}: expected a PEM-encoded RSA private key`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
if (key.asymmetricKeyType !== "rsa") {
|
|
185
|
-
throw new GitHubPluginSetupError(
|
|
186
|
-
`Invalid ${envName}: GitHub App signing requires an RSA private key`
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
return key;
|
|
190
|
-
}
|
|
191
|
-
function createAppJwt(appId, privateKeyEnv) {
|
|
192
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
193
|
-
const header = { alg: "RS256", typ: "JWT" };
|
|
194
|
-
const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
|
|
195
|
-
const encodedHeader = base64Url(JSON.stringify(header));
|
|
196
|
-
const encodedPayload = base64Url(JSON.stringify(payload));
|
|
197
|
-
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
198
|
-
const signer = createSign("RSA-SHA256");
|
|
199
|
-
signer.update(signingInput);
|
|
200
|
-
signer.end();
|
|
201
|
-
const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
202
|
-
return `${signingInput}.${signature}`;
|
|
203
|
-
}
|
|
204
|
-
async function githubRequest(apiBase, path, params) {
|
|
205
|
-
const response = await fetch(`${apiBase}${path}`, {
|
|
206
|
-
method: params.method ?? "GET",
|
|
207
|
-
headers: {
|
|
208
|
-
Accept: "application/vnd.github+json",
|
|
209
|
-
Authorization: `Bearer ${params.token}`,
|
|
210
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
211
|
-
...params.body ? { "Content-Type": "application/json" } : void 0
|
|
212
|
-
},
|
|
213
|
-
...params.body ? { body: JSON.stringify(params.body) } : void 0
|
|
214
|
-
});
|
|
215
|
-
const text2 = await response.text();
|
|
216
|
-
let parsed;
|
|
217
|
-
if (text2) {
|
|
218
|
-
try {
|
|
219
|
-
parsed = JSON.parse(text2);
|
|
220
|
-
} catch {
|
|
221
|
-
parsed = void 0;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (!response.ok) {
|
|
225
|
-
const message = isRecord(parsed) && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
|
|
226
|
-
throw new GitHubRequestError(message, response.status);
|
|
227
|
-
}
|
|
228
|
-
return parsed;
|
|
229
|
-
}
|
|
230
|
-
function buildOAuthTokenRequest(input) {
|
|
231
|
-
const payload = {
|
|
232
|
-
...input.payload,
|
|
233
|
-
client_id: input.clientId,
|
|
234
|
-
client_secret: input.clientSecret
|
|
235
|
-
};
|
|
236
|
-
return {
|
|
237
|
-
headers: {
|
|
238
|
-
Accept: "application/json",
|
|
239
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
240
|
-
},
|
|
241
|
-
body: new URLSearchParams(payload)
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
function parseOAuthResponseJson(responseText) {
|
|
245
|
-
if (!responseText.trim()) {
|
|
246
|
-
return void 0;
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
return JSON.parse(responseText);
|
|
250
|
-
} catch {
|
|
251
|
-
return void 0;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function oauthErrorCode(data) {
|
|
255
|
-
return isRecord(data) && typeof data.error === "string" ? data.error : void 0;
|
|
256
|
-
}
|
|
257
|
-
function isRejectedRefreshError(errorCode) {
|
|
258
|
-
return errorCode === "bad_refresh_token" || errorCode === "invalid_grant";
|
|
259
|
-
}
|
|
260
|
-
function parseOAuthTokenResponse(data, requestedScope) {
|
|
261
|
-
if (!isRecord(data)) {
|
|
262
|
-
throw new Error("OAuth token response is invalid");
|
|
263
|
-
}
|
|
264
|
-
if (typeof data.access_token !== "string" || !data.access_token.trim()) {
|
|
265
|
-
throw new Error("OAuth token response missing access_token");
|
|
266
|
-
}
|
|
267
|
-
if (typeof data.refresh_token !== "string" || !data.refresh_token.trim()) {
|
|
268
|
-
throw new Error("OAuth token response missing refresh_token");
|
|
269
|
-
}
|
|
270
|
-
let scope = normalizeOAuthScope(requestedScope);
|
|
271
|
-
if (data.scope !== void 0) {
|
|
272
|
-
if (typeof data.scope !== "string") {
|
|
273
|
-
throw new Error("OAuth token response returned invalid scope");
|
|
274
|
-
}
|
|
275
|
-
scope = normalizeOAuthScope(data.scope) ?? scope;
|
|
276
|
-
}
|
|
277
|
-
const result = {
|
|
278
|
-
accessToken: data.access_token,
|
|
279
|
-
refreshToken: data.refresh_token,
|
|
280
|
-
...scope ? { scope } : void 0
|
|
281
|
-
};
|
|
282
|
-
if (data.expires_in !== void 0) {
|
|
283
|
-
if (typeof data.expires_in !== "number" || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
284
|
-
throw new Error("OAuth token response returned invalid expires_in");
|
|
285
|
-
}
|
|
286
|
-
result.expiresAt = Date.now() + data.expires_in * 1e3;
|
|
287
|
-
}
|
|
288
|
-
if (data.refresh_token_expires_in !== void 0) {
|
|
289
|
-
if (typeof data.refresh_token_expires_in !== "number" || !Number.isFinite(data.refresh_token_expires_in) || data.refresh_token_expires_in <= 0) {
|
|
290
|
-
throw new Error(
|
|
291
|
-
"OAuth token response returned invalid refresh_token_expires_in"
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
result.refreshTokenExpiresAt = Date.now() + data.refresh_token_expires_in * 1e3;
|
|
295
|
-
}
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
async function refreshUserAccessToken(input) {
|
|
299
|
-
const clientId = requireEnv(input.clientIdEnv);
|
|
300
|
-
const clientSecret = requireEnv(input.clientSecretEnv);
|
|
301
|
-
const request = buildOAuthTokenRequest({
|
|
302
|
-
clientId,
|
|
303
|
-
clientSecret,
|
|
304
|
-
payload: {
|
|
305
|
-
grant_type: "refresh_token",
|
|
306
|
-
refresh_token: input.refreshToken
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
310
|
-
method: "POST",
|
|
311
|
-
headers: request.headers,
|
|
312
|
-
body: request.body,
|
|
313
|
-
signal: AbortSignal.timeout(USER_REFRESH_TIMEOUT_MS)
|
|
314
|
-
});
|
|
315
|
-
const responseText = await response.text();
|
|
316
|
-
const responseData = parseOAuthResponseJson(responseText);
|
|
317
|
-
const errorCode = oauthErrorCode(responseData);
|
|
318
|
-
if (isRejectedRefreshError(errorCode)) {
|
|
319
|
-
throw new GitHubUserRefreshRejectedError(
|
|
320
|
-
`GitHub user token refresh rejected: ${errorCode}`
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
if (!response.ok || errorCode) {
|
|
324
|
-
throw new Error(
|
|
325
|
-
`GitHub user token refresh failed: ${response.status}${errorCode ? ` ${errorCode}` : ""}`
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
return parseOAuthTokenResponse(responseData, input.requestedScope);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
if (error instanceof Error && error.message === "OAuth token response missing access_token") {
|
|
332
|
-
throw new GitHubUserRefreshRejectedError(error.message);
|
|
333
|
-
}
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function leaseExpiry(expiresAt) {
|
|
338
|
-
return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS) : Date.now() + MAX_LEASE_MS;
|
|
339
|
-
}
|
|
340
|
-
function isGitSmartHttpDomain(domain) {
|
|
341
|
-
return domain.toLowerCase() === "github.com";
|
|
342
|
-
}
|
|
343
|
-
function authorizationFor(domain, token) {
|
|
344
|
-
if (isGitSmartHttpDomain(domain)) {
|
|
345
|
-
return `Basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
|
|
346
|
-
}
|
|
347
|
-
return `Bearer ${token}`;
|
|
348
|
-
}
|
|
349
|
-
function createCredentialLease(input) {
|
|
350
|
-
return {
|
|
351
|
-
type: "lease",
|
|
352
|
-
lease: {
|
|
353
|
-
...input.account ? { account: input.account } : void 0,
|
|
354
|
-
...input.authorization ? { authorization: input.authorization } : void 0,
|
|
355
|
-
expiresAt: new Date(input.expiresAtMs).toISOString(),
|
|
356
|
-
headerTransforms: (input.domains ?? (input.authorization ? GITHUB_ASSET_UPLOAD_CREDENTIAL_DOMAINS : GITHUB_CREDENTIAL_DOMAINS)).map((domain) => ({
|
|
357
|
-
domain,
|
|
358
|
-
headers: {
|
|
359
|
-
Authorization: authorizationFor(domain, input.token)
|
|
360
|
-
}
|
|
361
|
-
}))
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function githubUserAuthorization(scope) {
|
|
366
|
-
return {
|
|
367
|
-
type: "oauth",
|
|
368
|
-
provider: "github",
|
|
369
|
-
...scope ? { scope } : void 0
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function credentialNeeded(message, scope, allowAuthorization = true) {
|
|
373
|
-
return {
|
|
374
|
-
type: "needed",
|
|
375
|
-
message,
|
|
376
|
-
...allowAuthorization ? { authorization: githubUserAuthorization(scope) } : void 0
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
function credentialUnavailable(message) {
|
|
380
|
-
return {
|
|
381
|
-
type: "unavailable",
|
|
382
|
-
message
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
function parseInstallationTokenResponse(data) {
|
|
386
|
-
if (!isRecord(data)) {
|
|
387
|
-
throw new Error("GitHub installation token response is invalid");
|
|
388
|
-
}
|
|
389
|
-
const token = data.token;
|
|
390
|
-
if (typeof token !== "string" || !token.trim()) {
|
|
391
|
-
throw new Error("GitHub installation token response missing token");
|
|
392
|
-
}
|
|
393
|
-
const expiresAt = data.expires_at;
|
|
394
|
-
const expiresAtMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN;
|
|
395
|
-
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
396
|
-
throw new Error(
|
|
397
|
-
"GitHub installation token response returned invalid expires_at"
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
return { token, expiresAtMs };
|
|
401
|
-
}
|
|
402
|
-
function readInstallationPermissions(installation) {
|
|
403
|
-
if (!isRecord(installation) || !isRecord(installation.permissions)) {
|
|
404
|
-
throw new Error("GitHub installation response missing permissions");
|
|
405
|
-
}
|
|
406
|
-
return readGrantPermissions(installation.permissions);
|
|
407
|
-
}
|
|
408
|
-
function decodeGitHubPathSegment(value) {
|
|
409
|
-
try {
|
|
410
|
-
const decoded = decodeURIComponent(value).trim();
|
|
411
|
-
return decoded && !decoded.includes("/") ? decoded : void 0;
|
|
412
|
-
} catch {
|
|
413
|
-
return void 0;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
function githubRepositoryFromUrl(upstreamUrl) {
|
|
417
|
-
const segments = upstreamUrl.pathname.split("/").filter(Boolean);
|
|
418
|
-
if (isGitHubApiUrl(upstreamUrl) && segments[0]?.toLowerCase() === "repos") {
|
|
419
|
-
const owner2 = segments[1] ? decodeGitHubPathSegment(segments[1]) : void 0;
|
|
420
|
-
const name2 = segments[2] ? decodeGitHubPathSegment(segments[2]) : void 0;
|
|
421
|
-
return owner2 && name2 ? { owner: owner2, name: name2 } : void 0;
|
|
422
|
-
}
|
|
423
|
-
if (upstreamUrl.hostname.toLowerCase() !== "github.com") {
|
|
424
|
-
return void 0;
|
|
425
|
-
}
|
|
426
|
-
const owner = segments[0] ? decodeGitHubPathSegment(segments[0]) : void 0;
|
|
427
|
-
const rawName = segments[1]?.replace(/\.git$/i, "");
|
|
428
|
-
const name = rawName ? decodeGitHubPathSegment(rawName) : void 0;
|
|
429
|
-
return owner && name ? { owner, name } : void 0;
|
|
430
|
-
}
|
|
431
|
-
function githubRepositoryLeaseScope(repository) {
|
|
432
|
-
return `repository:${repository.owner.toLowerCase()}/${repository.name.toLowerCase()}`;
|
|
433
|
-
}
|
|
434
|
-
function githubRepositoryFromLeaseScope(leaseScope) {
|
|
435
|
-
const match = /^repository:([^/]+)\/([^/]+)$/.exec(leaseScope ?? "");
|
|
436
|
-
if (!match?.[1] || !match[2]) {
|
|
437
|
-
throw new GitHubPluginSetupError(
|
|
438
|
-
"GitHub installation write grant is missing a repository lease scope."
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
return { owner: match[1], name: match[2] };
|
|
442
|
-
}
|
|
443
|
-
async function resolveUserAccount(tokens) {
|
|
444
|
-
const account = await githubRequest("https://api.github.com", "/user", {
|
|
445
|
-
token: tokens.accessToken
|
|
446
|
-
});
|
|
447
|
-
if (!isRecord(account)) {
|
|
448
|
-
throw new Error("GitHub user response is invalid");
|
|
449
|
-
}
|
|
450
|
-
const id = account.id;
|
|
451
|
-
const login = account.login;
|
|
452
|
-
if (typeof id !== "number" && typeof id !== "string" || typeof login !== "string" || !login.trim()) {
|
|
453
|
-
throw new Error("GitHub user response missing id or login");
|
|
454
|
-
}
|
|
455
|
-
const url = typeof account.html_url === "string" ? account.html_url : void 0;
|
|
456
|
-
return {
|
|
457
|
-
handle: login.trim(),
|
|
458
|
-
id: String(id),
|
|
459
|
-
label: login.trim(),
|
|
460
|
-
...url ? { url } : void 0
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
async function tokensWithAccount(tokenSlot, stored, scope) {
|
|
464
|
-
if (stored.account) {
|
|
465
|
-
return { ok: true, tokens: stored };
|
|
466
|
-
}
|
|
467
|
-
let account;
|
|
468
|
-
try {
|
|
469
|
-
account = await resolveUserAccount(stored);
|
|
470
|
-
} catch (error) {
|
|
471
|
-
if (error instanceof GitHubRequestError && (error.status === 401 || error.status === 403)) {
|
|
472
|
-
return {
|
|
473
|
-
ok: false,
|
|
474
|
-
result: credentialNeeded(
|
|
475
|
-
"Your GitHub authorization needs to be refreshed.",
|
|
476
|
-
scope
|
|
477
|
-
)
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
throw error;
|
|
481
|
-
}
|
|
482
|
-
const updated = { ...stored, account };
|
|
483
|
-
await tokenSlot.set(updated);
|
|
484
|
-
return { ok: true, tokens: updated };
|
|
485
|
-
}
|
|
486
|
-
function shouldRefreshUserToken(stored, now = Date.now()) {
|
|
487
|
-
return stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS;
|
|
488
|
-
}
|
|
489
|
-
function canUseStoredUserToken(stored) {
|
|
490
|
-
return stored.expiresAt === void 0 || stored.expiresAt > Date.now() && !shouldRefreshUserToken(stored);
|
|
491
|
-
}
|
|
492
|
-
async function refreshUserTokensWithLock(tokenSlot, scope, options) {
|
|
493
|
-
return await tokenSlot.withRefresh(async () => {
|
|
494
|
-
const latest = await tokenSlot.get();
|
|
495
|
-
if (!latest) {
|
|
496
|
-
return {
|
|
497
|
-
ok: false,
|
|
498
|
-
result: credentialNeeded("Connect your GitHub account.", scope)
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
if (!hasRequiredOAuthScope(latest.scope, scope)) {
|
|
502
|
-
return {
|
|
503
|
-
ok: false,
|
|
504
|
-
result: credentialNeeded(
|
|
505
|
-
"Your GitHub authorization needs to be refreshed.",
|
|
506
|
-
scope
|
|
507
|
-
)
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
if (canUseStoredUserToken(latest)) {
|
|
511
|
-
return { ok: true, tokens: latest };
|
|
512
|
-
}
|
|
513
|
-
let refreshed;
|
|
514
|
-
try {
|
|
515
|
-
refreshed = await refreshUserAccessToken({
|
|
516
|
-
clientIdEnv: options.clientIdEnv,
|
|
517
|
-
clientSecretEnv: options.clientSecretEnv,
|
|
518
|
-
refreshToken: latest.refreshToken,
|
|
519
|
-
requestedScope: latest.scope ?? scope
|
|
520
|
-
});
|
|
521
|
-
} catch (error) {
|
|
522
|
-
if (!(error instanceof GitHubUserRefreshRejectedError)) {
|
|
523
|
-
throw error;
|
|
524
|
-
}
|
|
525
|
-
return {
|
|
526
|
-
ok: false,
|
|
527
|
-
result: credentialNeeded(
|
|
528
|
-
"Your GitHub authorization has expired.",
|
|
529
|
-
scope
|
|
530
|
-
)
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
if (!hasRequiredOAuthScope(refreshed.scope, scope)) {
|
|
534
|
-
return {
|
|
535
|
-
ok: false,
|
|
536
|
-
result: credentialNeeded(
|
|
537
|
-
"Your GitHub authorization needs to be refreshed.",
|
|
538
|
-
scope
|
|
539
|
-
)
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const refreshedTokens = {
|
|
543
|
-
...latest.refreshTokenExpiresAt ? { refreshTokenExpiresAt: latest.refreshTokenExpiresAt } : void 0,
|
|
544
|
-
...refreshed,
|
|
545
|
-
...latest.account ? { account: latest.account } : void 0
|
|
546
|
-
};
|
|
547
|
-
await tokenSlot.set(refreshedTokens);
|
|
548
|
-
return { ok: true, tokens: refreshedTokens };
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
async function issueUserCredential(ctx, options) {
|
|
552
|
-
const scope = options.userScope;
|
|
553
|
-
const tokenSlot = ctx.tokens.currentUser ?? ctx.tokens.credentialSubject;
|
|
554
|
-
if (!tokenSlot) {
|
|
555
|
-
return credentialNeeded(
|
|
556
|
-
"GitHub write access requires a current user or delegated user credential subject.",
|
|
557
|
-
scope,
|
|
558
|
-
false
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
const stored = await tokenSlot.get();
|
|
562
|
-
if (!stored) {
|
|
563
|
-
return credentialNeeded(
|
|
564
|
-
"GitHub write access requires user authorization.",
|
|
565
|
-
scope
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
if (!hasRequiredOAuthScope(stored.scope, scope)) {
|
|
569
|
-
return credentialNeeded(
|
|
570
|
-
"Your GitHub authorization needs to be refreshed.",
|
|
571
|
-
scope
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
const now = Date.now();
|
|
575
|
-
if (stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS) {
|
|
576
|
-
const refreshResult = await refreshUserTokensWithLock(
|
|
577
|
-
tokenSlot,
|
|
578
|
-
scope,
|
|
579
|
-
options
|
|
580
|
-
);
|
|
581
|
-
if (!refreshResult.ok) {
|
|
582
|
-
return refreshResult.result;
|
|
583
|
-
}
|
|
584
|
-
const withAccount = await tokensWithAccount(
|
|
585
|
-
tokenSlot,
|
|
586
|
-
refreshResult.tokens,
|
|
587
|
-
scope
|
|
588
|
-
);
|
|
589
|
-
if (!withAccount.ok) {
|
|
590
|
-
return withAccount.result;
|
|
591
|
-
}
|
|
592
|
-
return createCredentialLease({
|
|
593
|
-
account: withAccount.tokens.account,
|
|
594
|
-
token: withAccount.tokens.accessToken,
|
|
595
|
-
expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
|
|
596
|
-
authorization: githubUserAuthorization(scope)
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
if (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) {
|
|
600
|
-
const withAccount = await tokensWithAccount(tokenSlot, stored, scope);
|
|
601
|
-
if (!withAccount.ok) {
|
|
602
|
-
return withAccount.result;
|
|
603
|
-
}
|
|
604
|
-
return createCredentialLease({
|
|
605
|
-
account: withAccount.tokens.account,
|
|
606
|
-
token: withAccount.tokens.accessToken,
|
|
607
|
-
expiresAtMs: leaseExpiry(withAccount.tokens.expiresAt),
|
|
608
|
-
authorization: githubUserAuthorization(scope)
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
return credentialNeeded("Your GitHub authorization has expired.", scope);
|
|
612
|
-
}
|
|
613
|
-
async function issueInstallationToken(options) {
|
|
614
|
-
const appId = requireEnv(options.appIdEnv);
|
|
615
|
-
const installationIdRaw = requireEnv(options.installationIdEnv);
|
|
616
|
-
const installationId = Number(installationIdRaw);
|
|
617
|
-
if (!Number.isSafeInteger(installationId) || installationId <= 0) {
|
|
618
|
-
throw new GitHubPluginSetupError(`Invalid ${options.installationIdEnv}`);
|
|
619
|
-
}
|
|
620
|
-
const appJwt = createAppJwt(appId, options.privateKeyEnv);
|
|
621
|
-
const permissions = "permissions" in options ? options.permissions : typeof options.loadPermissions === "function" ? await options.loadPermissions({ appJwt, installationId }) : void 0;
|
|
622
|
-
const body = {
|
|
623
|
-
...permissions ? { permissions } : void 0,
|
|
624
|
-
..."repositories" in options ? { repositories: options.repositories } : void 0
|
|
625
|
-
};
|
|
626
|
-
const accessTokenResponse = await githubRequest(
|
|
627
|
-
"https://api.github.com",
|
|
628
|
-
`/app/installations/${installationId}/access_tokens`,
|
|
629
|
-
{
|
|
630
|
-
method: "POST",
|
|
631
|
-
token: appJwt,
|
|
632
|
-
body
|
|
633
|
-
}
|
|
634
|
-
);
|
|
635
|
-
const parsedToken = parseInstallationTokenResponse(accessTokenResponse);
|
|
636
|
-
return {
|
|
637
|
-
expiresAtMs: Math.min(parsedToken.expiresAtMs, Date.now() + MAX_LEASE_MS),
|
|
638
|
-
token: parsedToken.token
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
async function issueInstallationCredential(options) {
|
|
642
|
-
const token = await issueInstallationToken(options);
|
|
643
|
-
return createCredentialLease({
|
|
644
|
-
token: token.token,
|
|
645
|
-
expiresAtMs: token.expiresAtMs
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
function createPermissionCache() {
|
|
649
|
-
let cached;
|
|
650
|
-
let pending;
|
|
651
|
-
return async ({ appJwt, installationId }) => {
|
|
652
|
-
if (cached && cached.expiresAtMs > Date.now()) {
|
|
653
|
-
return cached.permissions;
|
|
654
|
-
}
|
|
655
|
-
pending ??= githubRequest(
|
|
656
|
-
"https://api.github.com",
|
|
657
|
-
`/app/installations/${installationId}`,
|
|
658
|
-
{ token: appJwt }
|
|
659
|
-
).then((installation) => {
|
|
660
|
-
const permissions = readInstallationPermissions(installation);
|
|
661
|
-
cached = {
|
|
662
|
-
expiresAtMs: Date.now() + MAX_LEASE_MS,
|
|
663
|
-
permissions
|
|
664
|
-
};
|
|
665
|
-
return permissions;
|
|
666
|
-
}).finally(() => {
|
|
667
|
-
pending = void 0;
|
|
668
|
-
});
|
|
669
|
-
return await pending;
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// src/pull-request-review-policy.ts
|
|
674
|
-
function assertGitHubPullRequestApprovalDenied(input) {
|
|
675
|
-
if (input.method !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
const match = input.upstreamUrl.pathname.toLowerCase().match(
|
|
679
|
-
/^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/(events))?$/
|
|
680
|
-
);
|
|
681
|
-
if (!match) return;
|
|
682
|
-
const isEventsPath = match[1] === "events";
|
|
683
|
-
const bodyText = input.bodyText?.trim() ?? "";
|
|
684
|
-
if (!bodyText) {
|
|
685
|
-
if (isEventsPath) {
|
|
686
|
-
throw new EgressPolicyDenied(
|
|
687
|
-
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
let body;
|
|
693
|
-
try {
|
|
694
|
-
body = JSON.parse(bodyText);
|
|
695
|
-
} catch {
|
|
696
|
-
throw new EgressPolicyDenied(
|
|
697
|
-
"GitHub pull request review requests must use JSON bodies so Junior can enforce the no-approve policy."
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
if (!isRecord(body)) {
|
|
701
|
-
throw new EgressPolicyDenied(
|
|
702
|
-
"GitHub pull request review requests must use JSON object bodies so Junior can enforce the no-approve policy."
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
let event;
|
|
706
|
-
if ("event" in body) {
|
|
707
|
-
if (typeof body.event !== "string" || body.event.trim().length === 0) {
|
|
708
|
-
throw new EgressPolicyDenied(
|
|
709
|
-
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
event = body.event.trim().toUpperCase();
|
|
713
|
-
}
|
|
714
|
-
if (event === "APPROVE") {
|
|
715
|
-
throw new EgressPolicyDenied(
|
|
716
|
-
"Junior cannot approve GitHub pull requests. Request changes, leave a comment review, or dismiss Junior's own review instead."
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
if (isEventsPath && event === void 0) {
|
|
720
|
-
throw new EgressPolicyDenied(
|
|
721
|
-
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
54
|
// src/tools/clone-repository.ts
|
|
727
55
|
import {
|
|
728
56
|
definePluginTool,
|
|
@@ -3072,6 +2400,7 @@ var githubPullRequestOutcomeInputSchema = z13.object({
|
|
|
3072
2400
|
repositoryFullName: z13.string().min(1),
|
|
3073
2401
|
repositoryId: z13.string().min(1),
|
|
3074
2402
|
state: githubPullRequestStateSchema,
|
|
2403
|
+
title: z13.string().min(1).optional(),
|
|
3075
2404
|
updatedAt: z13.date()
|
|
3076
2405
|
}).strict();
|
|
3077
2406
|
var githubPullRequestConversationsInputSchema = z13.object({
|
|
@@ -3401,6 +2730,7 @@ var canonicalPullRequestOutcomeSchema = z15.object({
|
|
|
3401
2730
|
merged: z15.boolean(),
|
|
3402
2731
|
merged_at: z15.string().nullable().optional(),
|
|
3403
2732
|
number: z15.number().int().positive(),
|
|
2733
|
+
title: z15.string().min(1).optional(),
|
|
3404
2734
|
updated_at: z15.string(),
|
|
3405
2735
|
user: z15.object({ login: z15.string().min(1) }).strict()
|
|
3406
2736
|
}).strict(),
|
|
@@ -3419,6 +2749,7 @@ var pullRequestOutcomeSchema = z15.object({
|
|
|
3419
2749
|
merged: z15.boolean(),
|
|
3420
2750
|
merged_at: z15.string().nullable().optional(),
|
|
3421
2751
|
number: z15.number().int().positive(),
|
|
2752
|
+
title: z15.string().min(1).optional(),
|
|
3422
2753
|
updated_at: z15.string(),
|
|
3423
2754
|
user: z15.object({ login: z15.string().min(1) }).passthrough()
|
|
3424
2755
|
}).passthrough(),
|
|
@@ -3437,6 +2768,7 @@ var pullRequestOutcomeSchema = z15.object({
|
|
|
3437
2768
|
merged: provider.pull_request.merged,
|
|
3438
2769
|
merged_at: provider.pull_request.merged_at,
|
|
3439
2770
|
number: provider.pull_request.number,
|
|
2771
|
+
title: provider.pull_request.title,
|
|
3440
2772
|
updated_at: provider.pull_request.updated_at,
|
|
3441
2773
|
user: { login: provider.pull_request.user.login }
|
|
3442
2774
|
},
|
|
@@ -3519,6 +2851,7 @@ function normalizeGitHubPullRequestOutcome(args) {
|
|
|
3519
2851
|
repositoryFullName: parsed.repository.full_name,
|
|
3520
2852
|
repositoryId: String(parsed.repository.id),
|
|
3521
2853
|
state,
|
|
2854
|
+
title: pullRequest.title,
|
|
3522
2855
|
updatedAt
|
|
3523
2856
|
};
|
|
3524
2857
|
}
|
|
@@ -3582,6 +2915,25 @@ function webhookInstallationId(body) {
|
|
|
3582
2915
|
}
|
|
3583
2916
|
return parseInstallationId(installation.id);
|
|
3584
2917
|
}
|
|
2918
|
+
function githubCodeChange(outcome, conversationIds) {
|
|
2919
|
+
return {
|
|
2920
|
+
closedAt: outcome.closedAt,
|
|
2921
|
+
conversationIds,
|
|
2922
|
+
mergedAt: outcome.mergedAt,
|
|
2923
|
+
number: outcome.number,
|
|
2924
|
+
openedAt: outcome.openedAt,
|
|
2925
|
+
providerId: outcome.pullRequestId,
|
|
2926
|
+
repository: {
|
|
2927
|
+
name: outcome.repositoryFullName,
|
|
2928
|
+
providerId: outcome.repositoryId,
|
|
2929
|
+
url: `https://github.com/${outcome.repositoryFullName}`
|
|
2930
|
+
},
|
|
2931
|
+
state: outcome.state === "closed_unmerged" ? "closed" : outcome.state,
|
|
2932
|
+
title: outcome.title,
|
|
2933
|
+
updatedAt: outcome.updatedAt,
|
|
2934
|
+
url: `https://github.com/${outcome.repositoryFullName}/pull/${outcome.number}`
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
3585
2937
|
function createGitHubWebhookRoute(args) {
|
|
3586
2938
|
return {
|
|
3587
2939
|
method: "POST",
|
|
@@ -3618,6 +2970,14 @@ function createGitHubWebhookRoute(args) {
|
|
|
3618
2970
|
args.db,
|
|
3619
2971
|
pullRequestOutcome
|
|
3620
2972
|
);
|
|
2973
|
+
if (recordedOutcome.applied) {
|
|
2974
|
+
await args.codeChanges.record(
|
|
2975
|
+
githubCodeChange(
|
|
2976
|
+
pullRequestOutcome,
|
|
2977
|
+
recordedOutcome.conversationIds
|
|
2978
|
+
)
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
3621
2981
|
if (recordedOutcome.applied && pullRequestOutcome.state !== "open") {
|
|
3622
2982
|
const status = pullRequestOutcome.state === "merged" ? "merged" : "closed";
|
|
3623
2983
|
await Promise.all(
|
|
@@ -3679,16 +3039,31 @@ function createGitHubWebhookRoute(args) {
|
|
|
3679
3039
|
args.db,
|
|
3680
3040
|
pullRequestConversations
|
|
3681
3041
|
) : false;
|
|
3042
|
+
if (recordedPullRequestConversations && pullRequestConversations) {
|
|
3043
|
+
await args.codeChanges.associateConversations({
|
|
3044
|
+
conversationIds: pullRequestConversations.conversationIds,
|
|
3045
|
+
providerId: pullRequestConversations.pullRequestId
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3682
3048
|
const recordedPullRequestLinkedIssues = pullRequestLinkedIssues ? await recordGitHubPullRequestLinkedIssues(
|
|
3683
3049
|
args.db,
|
|
3684
3050
|
pullRequestLinkedIssues
|
|
3685
3051
|
) : false;
|
|
3686
|
-
const
|
|
3052
|
+
const checkSuitePublishTargets = eventName === "check_suite" ? parseCheckSuitePublishTargets(body) : void 0;
|
|
3053
|
+
const checkSuiteMatchKeys = checkSuitePublishTargets && args.resourceEvents.neededMatchKeys ? await args.resourceEvents.neededMatchKeys(checkSuitePublishTargets) : [];
|
|
3054
|
+
const checkSuiteFacts = eventName === "check_suite" ? await loadCheckSuiteFacts({
|
|
3055
|
+
appIdEnv: args.appIdEnv,
|
|
3056
|
+
body,
|
|
3057
|
+
installationIdEnv: args.installationIdEnv,
|
|
3058
|
+
loadPullRequestFacts: needsCheckSuitePullRequestFacts(checkSuiteMatchKeys),
|
|
3059
|
+
log: args.log,
|
|
3060
|
+
privateKeyEnv: args.privateKeyEnv
|
|
3061
|
+
}) : void 0;
|
|
3687
3062
|
const resourceEvents = normalizeGitHubResourceEvents({
|
|
3688
3063
|
body,
|
|
3064
|
+
...checkSuiteFacts ? { checkSuiteFacts } : void 0,
|
|
3689
3065
|
deliveryId,
|
|
3690
|
-
eventName
|
|
3691
|
-
failingChecks
|
|
3066
|
+
eventName
|
|
3692
3067
|
});
|
|
3693
3068
|
for (const event of resourceEvents) {
|
|
3694
3069
|
await args.resourceEvents.publish(event);
|
|
@@ -4852,44 +4227,6 @@ function githubSidebarAnnotations(annotations) {
|
|
|
4852
4227
|
}).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map(({ annotation }) => annotation);
|
|
4853
4228
|
}
|
|
4854
4229
|
|
|
4855
|
-
// src/webhooks/check-suite-enrichment.ts
|
|
4856
|
-
function checkRunsFromResponse(value) {
|
|
4857
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4858
|
-
return [];
|
|
4859
|
-
}
|
|
4860
|
-
const checkRuns = value.check_runs;
|
|
4861
|
-
return Array.isArray(checkRuns) ? checkRuns : [];
|
|
4862
|
-
}
|
|
4863
|
-
async function loadFailingChecksForSuite(args) {
|
|
4864
|
-
const target = parseCheckSuiteEnrichmentTarget(args.body);
|
|
4865
|
-
if (!target) return void 0;
|
|
4866
|
-
try {
|
|
4867
|
-
const token = await issueInstallationToken({
|
|
4868
|
-
appIdEnv: args.appIdEnv,
|
|
4869
|
-
installationIdEnv: args.installationIdEnv,
|
|
4870
|
-
permissions: { checks: "read" },
|
|
4871
|
-
privateKeyEnv: args.privateKeyEnv,
|
|
4872
|
-
repositories: [target.repoName]
|
|
4873
|
-
});
|
|
4874
|
-
const response = await githubRequest(
|
|
4875
|
-
"https://api.github.com",
|
|
4876
|
-
`/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repoName)}/check-suites/${target.checkSuiteId}/check-runs?filter=latest&per_page=100`,
|
|
4877
|
-
{ token: token.token }
|
|
4878
|
-
);
|
|
4879
|
-
const failing = selectFailingChecks(checkRunsFromResponse(response), {
|
|
4880
|
-
checkSuiteId: target.checkSuiteId
|
|
4881
|
-
});
|
|
4882
|
-
return failing.length > 0 ? failing : void 0;
|
|
4883
|
-
} catch (error) {
|
|
4884
|
-
args.log?.error("GitHub check suite enrichment failed", {
|
|
4885
|
-
checkSuiteId: target.checkSuiteId,
|
|
4886
|
-
errorType: error instanceof Error ? error.name : "UnknownError",
|
|
4887
|
-
repository: `${target.owner}/${target.repoName}`
|
|
4888
|
-
});
|
|
4889
|
-
return void 0;
|
|
4890
|
-
}
|
|
4891
|
-
}
|
|
4892
|
-
|
|
4893
4230
|
// src/git-config.ts
|
|
4894
4231
|
function cleanIdentityPart(value) {
|
|
4895
4232
|
return String(value ?? "").replaceAll("\n", " ").replaceAll("\r", " ").replace(/[<>]/g, "").trim();
|
|
@@ -5438,7 +4775,67 @@ async function prepareWorkspace(ctx) {
|
|
|
5438
4775
|
}
|
|
5439
4776
|
}
|
|
5440
4777
|
|
|
5441
|
-
// src/
|
|
4778
|
+
// src/egress-policy.ts
|
|
4779
|
+
import {
|
|
4780
|
+
EgressPolicyDenied as EgressPolicyDenied2,
|
|
4781
|
+
enforceEgressPolicy
|
|
4782
|
+
} from "@sentry/junior-plugin-api";
|
|
4783
|
+
|
|
4784
|
+
// src/pull-request-review-policy.ts
|
|
4785
|
+
import { EgressPolicyDenied } from "@sentry/junior-plugin-api";
|
|
4786
|
+
function assertGitHubPullRequestApprovalDenied(input) {
|
|
4787
|
+
if (input.method !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") {
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
const match = input.upstreamUrl.pathname.toLowerCase().match(
|
|
4791
|
+
/^\/repos\/[^/]+\/[^/]+\/pulls\/[^/]+\/reviews(?:\/[^/]+\/(events))?$/
|
|
4792
|
+
);
|
|
4793
|
+
if (!match) return;
|
|
4794
|
+
const isEventsPath = match[1] === "events";
|
|
4795
|
+
const bodyText = input.bodyText?.trim() ?? "";
|
|
4796
|
+
if (!bodyText) {
|
|
4797
|
+
if (isEventsPath) {
|
|
4798
|
+
throw new EgressPolicyDenied(
|
|
4799
|
+
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
4800
|
+
);
|
|
4801
|
+
}
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4804
|
+
let body;
|
|
4805
|
+
try {
|
|
4806
|
+
body = JSON.parse(bodyText);
|
|
4807
|
+
} catch {
|
|
4808
|
+
throw new EgressPolicyDenied(
|
|
4809
|
+
"GitHub pull request review requests must use JSON bodies so Junior can enforce the no-approve policy."
|
|
4810
|
+
);
|
|
4811
|
+
}
|
|
4812
|
+
if (!isRecord(body)) {
|
|
4813
|
+
throw new EgressPolicyDenied(
|
|
4814
|
+
"GitHub pull request review requests must use JSON object bodies so Junior can enforce the no-approve policy."
|
|
4815
|
+
);
|
|
4816
|
+
}
|
|
4817
|
+
let event;
|
|
4818
|
+
if ("event" in body) {
|
|
4819
|
+
if (typeof body.event !== "string" || body.event.trim().length === 0) {
|
|
4820
|
+
throw new EgressPolicyDenied(
|
|
4821
|
+
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
4822
|
+
);
|
|
4823
|
+
}
|
|
4824
|
+
event = body.event.trim().toUpperCase();
|
|
4825
|
+
}
|
|
4826
|
+
if (event === "APPROVE") {
|
|
4827
|
+
throw new EgressPolicyDenied(
|
|
4828
|
+
"Junior cannot approve GitHub pull requests. Request changes, leave a comment review, or dismiss Junior's own review instead."
|
|
4829
|
+
);
|
|
4830
|
+
}
|
|
4831
|
+
if (isEventsPath && event === void 0) {
|
|
4832
|
+
throw new EgressPolicyDenied(
|
|
4833
|
+
"GitHub pull request review submissions must include a parseable non-APPROVE event so Junior can enforce the no-approve policy."
|
|
4834
|
+
);
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4838
|
+
// src/egress-policy.ts
|
|
5442
4839
|
function githubSmartHttpAccess(upstreamUrl) {
|
|
5443
4840
|
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
5444
4841
|
const service = upstreamUrl.searchParams.get("service")?.toLowerCase();
|
|
@@ -5457,14 +4854,14 @@ function githubSmartHttpAccess(upstreamUrl) {
|
|
|
5457
4854
|
function isGitHubGraphqlUrl(upstreamUrl) {
|
|
5458
4855
|
return upstreamUrl.hostname.toLowerCase() === "api.github.com" && upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
|
|
5459
4856
|
}
|
|
5460
|
-
function
|
|
4857
|
+
function isGitHubApiUrl(upstreamUrl) {
|
|
5461
4858
|
return upstreamUrl.hostname.toLowerCase() === "api.github.com";
|
|
5462
4859
|
}
|
|
5463
4860
|
function isGitHubAssetUploadRequest(method, upstreamUrl) {
|
|
5464
4861
|
return method === "POST" && upstreamUrl.hostname.toLowerCase() === "uploads.github.com" && upstreamUrl.pathname === "/user-attachments/assets";
|
|
5465
4862
|
}
|
|
5466
4863
|
function githubUserReadReason(method, upstreamUrl) {
|
|
5467
|
-
if (method !== "GET" || !
|
|
4864
|
+
if (method !== "GET" || !isGitHubApiUrl(upstreamUrl)) {
|
|
5468
4865
|
return void 0;
|
|
5469
4866
|
}
|
|
5470
4867
|
return upstreamUrl.pathname.toLowerCase() === "/user" ? "github.user-read" : void 0;
|
|
@@ -5622,7 +5019,7 @@ var GITHUB_BODY_WRITES = [
|
|
|
5622
5019
|
];
|
|
5623
5020
|
function githubApiWriteGrantName(method, upstreamUrl) {
|
|
5624
5021
|
const pathname = upstreamUrl.pathname.toLowerCase();
|
|
5625
|
-
if (!
|
|
5022
|
+
if (!isGitHubApiUrl(upstreamUrl)) {
|
|
5626
5023
|
return void 0;
|
|
5627
5024
|
}
|
|
5628
5025
|
if (method === "POST" && /^\/repos\/[^/]+\/[^/]+\/actions\/workflows\/[^/]+\/dispatches$/.test(
|
|
@@ -5710,7 +5107,7 @@ function applyGitHubEgressPolicy(input) {
|
|
|
5710
5107
|
});
|
|
5711
5108
|
const pathname = input.upstreamUrl.pathname.toLowerCase();
|
|
5712
5109
|
const write = GITHUB_BODY_WRITES.find(
|
|
5713
|
-
(candidate) => candidate.method === input.method &&
|
|
5110
|
+
(candidate) => candidate.method === input.method && isGitHubApiUrl(input.upstreamUrl) && candidate.restPath.test(pathname) || isGitHubGraphqlMutation(
|
|
5714
5111
|
input.method,
|
|
5715
5112
|
input.upstreamUrl,
|
|
5716
5113
|
input.bodyText,
|
|
@@ -5821,6 +5218,8 @@ async function githubGrantForEgress(ctx) {
|
|
|
5821
5218
|
}
|
|
5822
5219
|
return grantForAccess(access, "github.api-read", "installation-read");
|
|
5823
5220
|
}
|
|
5221
|
+
|
|
5222
|
+
// src/plugin.ts
|
|
5824
5223
|
function githubPlugin(options = {}) {
|
|
5825
5224
|
const botNameEnv = options.botNameEnv ?? "GITHUB_APP_BOT_NAME";
|
|
5826
5225
|
const botEmailEnv = options.botEmailEnv ?? "GITHUB_APP_BOT_EMAIL";
|
|
@@ -5852,6 +5251,7 @@ function githubPlugin(options = {}) {
|
|
|
5852
5251
|
type: "pull_request",
|
|
5853
5252
|
supportedEvents: [...GITHUB_PULL_REQUEST_EVENTS],
|
|
5854
5253
|
suggestedEvents: [...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS],
|
|
5254
|
+
matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS,
|
|
5855
5255
|
...options.pullRequestEvents?.guidance ? { guidance: options.pullRequestEvents.guidance } : void 0
|
|
5856
5256
|
},
|
|
5857
5257
|
{
|
|
@@ -5861,16 +5261,14 @@ function githubPlugin(options = {}) {
|
|
|
5861
5261
|
},
|
|
5862
5262
|
{
|
|
5863
5263
|
type: "repository",
|
|
5864
|
-
supportedEvents: [
|
|
5865
|
-
...GITHUB_ISSUE_EVENTS,
|
|
5866
|
-
...GITHUB_PULL_REQUEST_EVENTS
|
|
5867
|
-
],
|
|
5264
|
+
supportedEvents: [...GITHUB_ISSUE_EVENTS, ...GITHUB_PULL_REQUEST_EVENTS],
|
|
5868
5265
|
suggestedEvents: [
|
|
5869
5266
|
"issue.opened",
|
|
5870
5267
|
"pull_request.opened",
|
|
5871
5268
|
...GITHUB_ISSUE_SUGGESTED_EVENTS,
|
|
5872
5269
|
...GITHUB_PULL_REQUEST_SUGGESTED_EVENTS
|
|
5873
|
-
]
|
|
5270
|
+
],
|
|
5271
|
+
matchFields: GITHUB_PULL_REQUEST_MATCH_FIELDS
|
|
5874
5272
|
}
|
|
5875
5273
|
],
|
|
5876
5274
|
isEnabled: () => Boolean(readEnv("GITHUB_WEBHOOK_SECRET")),
|
|
@@ -5959,6 +5357,7 @@ function githubPlugin(options = {}) {
|
|
|
5959
5357
|
return [
|
|
5960
5358
|
createGitHubWebhookRoute({
|
|
5961
5359
|
annotations: ctx.annotations,
|
|
5360
|
+
appIdEnv,
|
|
5962
5361
|
botEmail: () => readEnv(botEmailEnv),
|
|
5963
5362
|
classifyPullRequestCommits: async ({
|
|
5964
5363
|
number,
|
|
@@ -5986,16 +5385,12 @@ function githubPlugin(options = {}) {
|
|
|
5986
5385
|
)
|
|
5987
5386
|
});
|
|
5988
5387
|
},
|
|
5388
|
+
codeChanges: ctx.codeChanges,
|
|
5989
5389
|
db: ctx.db,
|
|
5990
5390
|
installationId: () => readEnv(installationIdEnv),
|
|
5991
|
-
|
|
5992
|
-
appIdEnv,
|
|
5993
|
-
body,
|
|
5994
|
-
installationIdEnv,
|
|
5995
|
-
log: ctx.log,
|
|
5996
|
-
privateKeyEnv
|
|
5997
|
-
}),
|
|
5391
|
+
installationIdEnv,
|
|
5998
5392
|
log: ctx.log,
|
|
5393
|
+
privateKeyEnv,
|
|
5999
5394
|
resourceEvents: ctx.resourceEvents,
|
|
6000
5395
|
webhookSecret: () => readEnv("GITHUB_WEBHOOK_SECRET")
|
|
6001
5396
|
})
|