@useairfoil/producer-github 0.0.1 → 0.0.3
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/LICENSE.txt +201 -0
- package/README.md +116 -0
- package/dist/index.d.mts +1939 -0
- package/dist/index.mjs +3 -0
- package/dist/main.d.mts +1 -0
- package/dist/main.mjs +700 -0
- package/dist/main.mjs.map +1 -0
- package/dist/manifest.d.mts +56 -0
- package/dist/manifest.mjs +137 -0
- package/dist/manifest.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/src-DKDUc00e.mjs +1563 -0
- package/dist/src-DKDUc00e.mjs.map +1 -0
- package/package.json +46 -8
- package/src/client/auth.ts +113 -0
- package/src/client/client.ts +192 -0
- package/src/client/constants.ts +4 -0
- package/src/connector.ts +97 -0
- package/src/create-table.ts +10 -0
- package/src/demo/changes.ts +420 -0
- package/src/demo/index.ts +59 -0
- package/src/demo/setup.ts +34 -0
- package/src/demo/shared.ts +101 -0
- package/src/index.ts +13 -0
- package/src/main.ts +28 -0
- package/src/manifest.ts +45 -0
- package/src/resources/check-runs.ts +72 -0
- package/src/resources/commit-statuses.ts +84 -0
- package/src/resources/commits.ts +101 -0
- package/src/resources/issue-comments.ts +75 -0
- package/src/resources/issues.ts +78 -0
- package/src/resources/pull-request-commits.ts +91 -0
- package/src/resources/pull-request-review-comments.ts +78 -0
- package/src/resources/pull-request-reviews.ts +76 -0
- package/src/resources/pull-requests.ts +79 -0
- package/src/resources/releases.ts +74 -0
- package/src/resources/repositories.ts +101 -0
- package/src/resources/shared.ts +480 -0
- package/src/resources/workflow-jobs.ts +73 -0
- package/src/resources/workflow-runs.ts +70 -0
- package/src/sandbox.ts +35 -0
- package/src/schemas/check-runs.ts +72 -0
- package/src/schemas/commit-statuses.ts +51 -0
- package/src/schemas/commits.ts +80 -0
- package/src/schemas/events.ts +139 -0
- package/src/schemas/issue-comments.ts +48 -0
- package/src/schemas/issues.ts +76 -0
- package/src/schemas/pull-request-commits.ts +42 -0
- package/src/schemas/pull-request-review-comments.ts +85 -0
- package/src/schemas/pull-request-reviews.ts +53 -0
- package/src/schemas/pull-requests.ts +95 -0
- package/src/schemas/releases.ts +70 -0
- package/src/schemas/repositories.ts +73 -0
- package/src/schemas/shared.ts +76 -0
- package/src/schemas/workflow-jobs.ts +78 -0
- package/src/schemas/workflow-runs.ts +72 -0
- package/src/start.ts +47 -0
- package/src/tables.ts +31 -0
- package/src/webhook/route.ts +46 -0
- package/src/webhook/signature.ts +20 -0
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { GitHubConfigDef, manifest } from "./manifest.mjs";
|
|
3
|
+
import { Auth, Connector, ConnectorError, Cursor, Fetch, Iceberg, Metrics, Resource, Telemetry, Webhook } from "@useairfoil/connector-kit";
|
|
4
|
+
import { Array, Clock, Config, Context, DateTime, Duration, Effect, Exit, Layer, Option, Order, Redacted, Result, Schedule, Schema, SchemaTransformation, Stream, Struct } from "effect";
|
|
5
|
+
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/http";
|
|
6
|
+
import { createHmac, createPrivateKey, sign, timingSafeEqual } from "node:crypto";
|
|
7
|
+
//#region src/client/constants.ts
|
|
8
|
+
const GITHUB_API_VERSION = "2026-03-10";
|
|
9
|
+
const baseUrl = "https://api.github.com";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/client/auth.ts
|
|
12
|
+
var auth_exports = /* @__PURE__ */ __exportAll({
|
|
13
|
+
GitHubAuth: () => GitHubAuth,
|
|
14
|
+
layer: () => layer$2,
|
|
15
|
+
make: () => make$2,
|
|
16
|
+
makeAppJwt: () => makeAppJwt
|
|
17
|
+
});
|
|
18
|
+
var GitHubAuth = class extends Context.Service()("@useairfoil/producer-github/GitHubAuth") {};
|
|
19
|
+
const TokenResponseSchema = Schema.Struct({
|
|
20
|
+
token: Schema.String,
|
|
21
|
+
expires_at: Schema.DateFromString
|
|
22
|
+
});
|
|
23
|
+
const base64Url = (value) => Buffer.from(value).toString("base64url");
|
|
24
|
+
/**
|
|
25
|
+
* Signs the app JWT with RS256. GitHub allows at most 10 minutes, and asks for
|
|
26
|
+
* `iat` 60 seconds in the past to allow for clock drift.
|
|
27
|
+
*/
|
|
28
|
+
const makeAppJwt = (options) => Effect.try({
|
|
29
|
+
try: () => {
|
|
30
|
+
const now = Math.floor(options.nowMillis / 1e3);
|
|
31
|
+
const signingInput = `${base64Url(JSON.stringify({
|
|
32
|
+
alg: "RS256",
|
|
33
|
+
typ: "JWT"
|
|
34
|
+
}))}.${base64Url(JSON.stringify({
|
|
35
|
+
iat: now - 60,
|
|
36
|
+
exp: now + 540,
|
|
37
|
+
iss: options.clientId
|
|
38
|
+
}))}`;
|
|
39
|
+
const key = createPrivateKey(options.privateKey.replace(/\\n/g, "\n"));
|
|
40
|
+
return `${signingInput}.${base64Url(sign("sha256", Buffer.from(signingInput), key))}`;
|
|
41
|
+
},
|
|
42
|
+
catch: (cause) => new ConnectorError({
|
|
43
|
+
message: "Invalid GitHub App private key",
|
|
44
|
+
cause
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
const make$2 = Effect.fnUntraced(function* (config) {
|
|
48
|
+
const client = (yield* HttpClient.HttpClient).pipe(HttpClient.retryTransient({
|
|
49
|
+
schedule: Schedule.exponential("250 millis").pipe(Schedule.jittered),
|
|
50
|
+
times: 3
|
|
51
|
+
}));
|
|
52
|
+
const path = `/app/installations/${config.installationId}/access_tokens`;
|
|
53
|
+
return yield* Auth.makeTokenCache({ acquire: Effect.gen(function* () {
|
|
54
|
+
const jwt = yield* makeAppJwt({
|
|
55
|
+
clientId: config.appClientId,
|
|
56
|
+
privateKey: Redacted.value(config.appPrivateKey),
|
|
57
|
+
nowMillis: yield* Clock.currentTimeMillis
|
|
58
|
+
});
|
|
59
|
+
const request = HttpClientRequest.post(`${baseUrl}${path}`).pipe(HttpClientRequest.bearerToken(jwt), HttpClientRequest.acceptJson, HttpClientRequest.setHeader("X-GitHub-Api-Version", GITHUB_API_VERSION));
|
|
60
|
+
const response = yield* client.execute(request).pipe(Effect.mapError((cause) => new ConnectorError({
|
|
61
|
+
message: "GitHub installation token request failed",
|
|
62
|
+
cause
|
|
63
|
+
})));
|
|
64
|
+
const body = yield* HttpClientResponse.matchStatus(response, {
|
|
65
|
+
"2xx": (ok) => HttpClientResponse.schemaBodyJson(TokenResponseSchema)(ok).pipe(Effect.mapError((cause) => new ConnectorError({
|
|
66
|
+
message: "Invalid GitHub installation token response",
|
|
67
|
+
cause
|
|
68
|
+
}))),
|
|
69
|
+
orElse: (failed) => Effect.fail(new ConnectorError({ message: `GitHub returned ${failed.status} for the installation token` }))
|
|
70
|
+
});
|
|
71
|
+
const now = yield* Clock.currentTimeMillis;
|
|
72
|
+
return {
|
|
73
|
+
value: Redacted.make(body.token),
|
|
74
|
+
expiresIn: Duration.millis(Math.max(body.expires_at.getTime() - now, 0))
|
|
75
|
+
};
|
|
76
|
+
}).pipe(Effect.tapError((error) => Telemetry.annotateError("api_auth", error)), Effect.withSpan(Telemetry.SpanName.apiFetch, {
|
|
77
|
+
kind: "client",
|
|
78
|
+
attributes: { [Telemetry.Attr.apiPath]: path }
|
|
79
|
+
})) });
|
|
80
|
+
});
|
|
81
|
+
const layer$2 = (config) => Layer.effect(GitHubAuth)(make$2(config));
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/client/client.ts
|
|
84
|
+
var client_exports = /* @__PURE__ */ __exportAll({
|
|
85
|
+
GitHubClient: () => GitHubClient,
|
|
86
|
+
layer: () => layer$1,
|
|
87
|
+
layerConfig: () => layerConfig$1,
|
|
88
|
+
make: () => make$1,
|
|
89
|
+
nextLink: () => nextLink,
|
|
90
|
+
withParams: () => withParams
|
|
91
|
+
});
|
|
92
|
+
const maxRetries = 5;
|
|
93
|
+
const retryBaseDelay = Duration.millis(500);
|
|
94
|
+
const requestTimeout = Duration.minutes(2);
|
|
95
|
+
var GitHubClient = class extends Context.Service()("@useairfoil/producer-github/GitHubClient") {};
|
|
96
|
+
const withParams = (path, params) => `${path}?${new URLSearchParams(params).toString()}`;
|
|
97
|
+
const nextLink = (header) => Option.fromNullishOr(/<([^>]+)>;\s*rel="next"/.exec(header ?? "")?.[1]);
|
|
98
|
+
const ErrorBodySchema = Schema.Struct({ message: Schema.optional(Schema.String) });
|
|
99
|
+
const statusError = (response, path) => HttpClientResponse.schemaBodyJson(ErrorBodySchema)(response).pipe(Effect.map((body) => body.message), Effect.orElseSucceed(() => void 0), Effect.flatMap((detail) => Effect.fail(new ConnectorError({ message: `GitHub API returned ${response.status} for ${path}${detail ? `: ${detail}` : ""}` }))), Effect.tapError((error) => Telemetry.annotateError("api_status", error)));
|
|
100
|
+
/**
|
|
101
|
+
* GitHub answers rate limits with `403` or `429`. A `403` is a rate limit when
|
|
102
|
+
* it has `retry-after`, `x-ratelimit-remaining: 0`, or a rate limit message;
|
|
103
|
+
* otherwise it's a permission error.
|
|
104
|
+
*/
|
|
105
|
+
const isRateLimited = (response, body) => response.status === 429 || response.status === 403 && (response.headers["retry-after"] !== void 0 || response.headers["x-ratelimit-remaining"] === "0" || /rate limit/i.test(body));
|
|
106
|
+
const rateLimitedResponse = (input) => HttpClientError.isHttpClientError(input) && input.reason._tag === "StatusCodeError" ? input.reason.response : void 0;
|
|
107
|
+
const retrySchedule = Metrics.retrySchedule({
|
|
108
|
+
connector: manifest.name,
|
|
109
|
+
baseDelay: retryBaseDelay,
|
|
110
|
+
times: maxRetries
|
|
111
|
+
}).pipe(Schedule.modifyDelay(({ input, duration, now, attempt }) => {
|
|
112
|
+
const response = rateLimitedResponse(input);
|
|
113
|
+
if (response === void 0) return Effect.succeed(duration);
|
|
114
|
+
const retryAfter = response.headers["retry-after"];
|
|
115
|
+
if (retryAfter !== void 0) return Effect.succeed(Duration.seconds(Number(retryAfter)));
|
|
116
|
+
if (response.headers["x-ratelimit-remaining"] === "0") return Effect.succeed(Duration.millis(Math.max(Number(response.headers["x-ratelimit-reset"]) * 1e3 - now, 0) + 1e3));
|
|
117
|
+
return Effect.succeed(Duration.minutes(2 ** (attempt - 1)));
|
|
118
|
+
}));
|
|
119
|
+
const make$1 = Effect.fnUntraced(function* () {
|
|
120
|
+
const auth = yield* GitHubAuth;
|
|
121
|
+
const client = (yield* HttpClient.HttpClient).pipe(HttpClient.mapRequest((request) => request.pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeader("X-GitHub-Api-Version", GITHUB_API_VERSION))), HttpClient.mapRequestEffect((request) => auth.get.pipe(Effect.map((token) => HttpClientRequest.bearerToken(request, token)))), HttpClient.transformResponse(Effect.flatMap((response) => Effect.flatMap(response.text, (body) => isRateLimited(response, body) ? Effect.fail(new HttpClientError.HttpClientError({ reason: new HttpClientError.StatusCodeError({
|
|
122
|
+
request: response.request,
|
|
123
|
+
response
|
|
124
|
+
}) })) : Effect.succeed(response)))), HttpClient.transform((effect) => Effect.timeout(effect, requestTimeout)), HttpClient.retryTransient({
|
|
125
|
+
schedule: retrySchedule,
|
|
126
|
+
while: (error) => rateLimitedResponse(error) !== void 0
|
|
127
|
+
}));
|
|
128
|
+
const get = (schema, url) => {
|
|
129
|
+
const fullUrl = url.startsWith("/") ? `${baseUrl}${url}` : url;
|
|
130
|
+
if (!fullUrl.startsWith(`https://api.github.com/`)) return Effect.fail(new ConnectorError({ message: `Not a GitHub API URL: ${url}` }));
|
|
131
|
+
const path = fullUrl.slice(22).split("?")[0] ?? fullUrl;
|
|
132
|
+
return client.execute(HttpClientRequest.get(fullUrl)).pipe(Effect.catchTag(["HttpClientError", "TimeoutError"], (error) => {
|
|
133
|
+
const response = rateLimitedResponse(error);
|
|
134
|
+
if (response !== void 0) return Effect.succeed(response);
|
|
135
|
+
return Telemetry.annotateError("api_http", error).pipe(Effect.andThen(Effect.fail(new ConnectorError({
|
|
136
|
+
message: `GitHub API request ${error._tag === "TimeoutError" ? "timed out" : "failed"} for ${path}`,
|
|
137
|
+
cause: error
|
|
138
|
+
}))));
|
|
139
|
+
}), Effect.flatMap((response) => HttpClientResponse.matchStatus(response, {
|
|
140
|
+
"2xx": (ok) => HttpClientResponse.schemaBodyJson(schema)(ok).pipe(Effect.tapError((error) => Telemetry.annotateError("api_decode", error)), Effect.mapError((cause) => new ConnectorError({
|
|
141
|
+
message: `Invalid GitHub API response for ${path}`,
|
|
142
|
+
cause
|
|
143
|
+
})), Effect.map((body) => ({
|
|
144
|
+
body,
|
|
145
|
+
next: nextLink(ok.headers.link)
|
|
146
|
+
}))),
|
|
147
|
+
orElse: (failed) => statusError(failed, path)
|
|
148
|
+
})), Effect.withSpan(Telemetry.SpanName.apiFetch, {
|
|
149
|
+
kind: "client",
|
|
150
|
+
attributes: {
|
|
151
|
+
[Telemetry.Attr.apiPath]: path,
|
|
152
|
+
"github.api.version": GITHUB_API_VERSION
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
};
|
|
156
|
+
return GitHubClient.of({ get });
|
|
157
|
+
});
|
|
158
|
+
const layer$1 = (config) => Layer.effect(GitHubClient)(make$1()).pipe(Layer.provide(layer$2(config)));
|
|
159
|
+
const layerConfig$1 = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer$1)));
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/schemas/shared.ts
|
|
162
|
+
/** IDs and counts. Connector Kit maps whole numbers to Iceberg longs. */
|
|
163
|
+
const Long = Schema.Int.pipe(Schema.decodeTo(Schema.BigInt, SchemaTransformation.transform({
|
|
164
|
+
decode: (value) => BigInt(value),
|
|
165
|
+
encode: (value) => Number(value)
|
|
166
|
+
})));
|
|
167
|
+
const Timestamp = Schema.DateFromString;
|
|
168
|
+
/** Text GitHub can clear, such as a body. */
|
|
169
|
+
const Text = Schema.NullOr(Schema.String).pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({
|
|
170
|
+
decode: (value) => value ?? "",
|
|
171
|
+
encode: (value) => value
|
|
172
|
+
})));
|
|
173
|
+
/** `null` becomes an empty list. A webhook can leave the list out. */
|
|
174
|
+
const optionalList = (item) => Schema.optionalKey(Schema.NullOr(Schema.Array(item)).pipe(Schema.decodeTo(Schema.Array(Schema.toType(item)), SchemaTransformation.transform({
|
|
175
|
+
decode: (value) => value ?? [],
|
|
176
|
+
encode: (value) => value
|
|
177
|
+
}))));
|
|
178
|
+
const user = Schema.Struct({
|
|
179
|
+
id: Long.pipe(Schema.annotateKey({ description: "GitHub user ID." })),
|
|
180
|
+
login: Schema.String.pipe(Schema.annotateKey({ description: "GitHub username." })),
|
|
181
|
+
type: Schema.String.pipe(Schema.annotateKey({ description: "Account type, such as User, Bot, or Organization." }))
|
|
182
|
+
});
|
|
183
|
+
const label = Schema.Struct({
|
|
184
|
+
id: Long.pipe(Schema.annotateKey({ description: "Label ID." })),
|
|
185
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Label name." }))
|
|
186
|
+
});
|
|
187
|
+
const milestone = Schema.Struct({
|
|
188
|
+
id: Long.pipe(Schema.annotateKey({ description: "Milestone ID." })),
|
|
189
|
+
number: Long.pipe(Schema.annotateKey({ description: "Milestone number in the repository." })),
|
|
190
|
+
title: Schema.String.pipe(Schema.annotateKey({ description: "Milestone title." }))
|
|
191
|
+
});
|
|
192
|
+
const version = (object) => Schema.Date.pipe(Schema.annotateKey({ description: `Time GitHub last updated the ${object}.` }));
|
|
193
|
+
/** For objects without an update time. A newer read wins. */
|
|
194
|
+
const readVersion = (object) => Schema.Date.pipe(Schema.annotateKey({ description: `Time the connector read the ${object}. GitHub has no update time for it.` }));
|
|
195
|
+
const deleted = (object) => Schema.optional(Schema.Boolean).pipe(Schema.annotateKey({ description: `Whether the ${object} was deleted in GitHub.` }));
|
|
196
|
+
const ObjectRefSchema = Schema.Struct({ id: Long });
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/schemas/check-runs.ts
|
|
199
|
+
const CheckRunSchema = Schema.Struct({
|
|
200
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique check run ID." })),
|
|
201
|
+
version: readVersion("check run"),
|
|
202
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the check run." })),
|
|
203
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the check run belongs to." })),
|
|
204
|
+
head_sha: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the check ran on." })),
|
|
205
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Check name." })),
|
|
206
|
+
status: Schema.String.pipe(Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." })),
|
|
207
|
+
conclusion: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Result of a completed check, such as success or failure." })),
|
|
208
|
+
started_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the check started." })),
|
|
209
|
+
completed_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the check completed." })),
|
|
210
|
+
app: Schema.NullOr(Schema.Struct({
|
|
211
|
+
id: Long.pipe(Schema.annotateKey({ description: "GitHub App ID." })),
|
|
212
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "GitHub App name." }))
|
|
213
|
+
})).pipe(Schema.annotateKey({ description: "GitHub App that created the check, such as GitHub Actions." })),
|
|
214
|
+
check_suite: Schema.NullOr(Schema.Struct({ id: Long.pipe(Schema.annotateKey({ description: "Check suite ID." })) })).pipe(Schema.annotateKey({ description: "Check suite the check run belongs to." })),
|
|
215
|
+
output: Schema.Struct({
|
|
216
|
+
title: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Output title." })),
|
|
217
|
+
summary: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Output summary in Markdown." }))
|
|
218
|
+
}).pipe(Schema.annotateKey({ description: "Output the check reported." })),
|
|
219
|
+
external_id: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "ID the app gave the check." })),
|
|
220
|
+
details_url: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Details page from the app." })),
|
|
221
|
+
html_url: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Check run page on GitHub." }))
|
|
222
|
+
}).annotate({ description: "Check runs, such as GitHub Actions jobs. Polled for default-branch commits; webhooks cover all branches." }).pipe(Iceberg.identifier("id"));
|
|
223
|
+
const CheckRunObjectSchema = CheckRunSchema.mapFields((fields) => Struct.omit(fields, ["version", "repository_id"]));
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/schemas/issue-comments.ts
|
|
226
|
+
const IssueCommentSchema = Schema.Struct({
|
|
227
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique comment ID." })),
|
|
228
|
+
version: version("comment"),
|
|
229
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the comment." })),
|
|
230
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the comment belongs to." })),
|
|
231
|
+
issue_number: Long.pipe(Schema.annotateKey({ description: "Number of the issue or pull request the comment is on, in its repository." })),
|
|
232
|
+
user: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that wrote the comment." })),
|
|
233
|
+
author_association: Schema.String.pipe(Schema.annotateKey({ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR." })),
|
|
234
|
+
body: Text.pipe(Schema.annotateKey({ description: "Comment in Markdown. Empty when not set." })),
|
|
235
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the comment was written." })),
|
|
236
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Comment on GitHub." })),
|
|
237
|
+
_deleted: deleted("comment")
|
|
238
|
+
}).annotate({ description: "Comments on issues and pull requests. Pull request review comments are not included." }).pipe(Iceberg.identifier("id"));
|
|
239
|
+
const IssueCommentObjectSchema = IssueCommentSchema.mapFields((fields) => ({
|
|
240
|
+
...Struct.omit(fields, [
|
|
241
|
+
"version",
|
|
242
|
+
"repository_id",
|
|
243
|
+
"issue_number",
|
|
244
|
+
"_deleted"
|
|
245
|
+
]),
|
|
246
|
+
updated_at: Timestamp,
|
|
247
|
+
issue_url: Schema.String.check(Schema.isPattern(/\/issues\/\d+$/))
|
|
248
|
+
}));
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/schemas/issues.ts
|
|
251
|
+
const IssueSchema = Schema.Struct({
|
|
252
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique issue ID." })),
|
|
253
|
+
version: version("issue"),
|
|
254
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the issue." })),
|
|
255
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the issue belongs to." })),
|
|
256
|
+
number: Long.pipe(Schema.annotateKey({ description: "Issue number in the repository." })),
|
|
257
|
+
title: Schema.String.pipe(Schema.annotateKey({ description: "Issue title." })),
|
|
258
|
+
body: Text.pipe(Schema.annotateKey({ description: "Issue description in Markdown. Empty when not set." })),
|
|
259
|
+
state: Schema.String.pipe(Schema.annotateKey({ description: "State: open or closed." })),
|
|
260
|
+
state_reason: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Why the issue was closed or reopened, such as completed or not_planned." })),
|
|
261
|
+
locked: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the conversation is locked." })),
|
|
262
|
+
user: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that opened the issue." })),
|
|
263
|
+
author_association: Schema.String.pipe(Schema.annotateKey({ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR." })),
|
|
264
|
+
labels: Schema.Array(label).pipe(Schema.annotateKey({ description: "Labels on the issue." })),
|
|
265
|
+
assignees: Schema.Array(user).pipe(Schema.annotateKey({ description: "Accounts assigned to the issue." })),
|
|
266
|
+
milestone: Schema.NullOr(milestone).pipe(Schema.annotateKey({ description: "Milestone of the issue." })),
|
|
267
|
+
comments: Long.pipe(Schema.annotateKey({ description: "Number of comments." })),
|
|
268
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the issue was opened." })),
|
|
269
|
+
closed_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the issue was closed. Null while it is open." })),
|
|
270
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Issue page on GitHub." })),
|
|
271
|
+
_deleted: deleted("issue")
|
|
272
|
+
}).annotate({ description: "Issues in the installation's repositories. Pull requests are not included." }).pipe(Iceberg.identifier("id"));
|
|
273
|
+
const IssueObjectSchema = IssueSchema.mapFields((fields) => ({
|
|
274
|
+
...Struct.omit(fields, [
|
|
275
|
+
"version",
|
|
276
|
+
"repository_id",
|
|
277
|
+
"labels",
|
|
278
|
+
"assignees",
|
|
279
|
+
"_deleted"
|
|
280
|
+
]),
|
|
281
|
+
updated_at: Timestamp,
|
|
282
|
+
labels: optionalList(label),
|
|
283
|
+
assignees: optionalList(user),
|
|
284
|
+
pull_request: Schema.optionalKey(Schema.Unknown)
|
|
285
|
+
}));
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/schemas/pull-request-review-comments.ts
|
|
288
|
+
const PullRequestReviewCommentSchema = Schema.Struct({
|
|
289
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique review comment ID." })),
|
|
290
|
+
version: version("review comment"),
|
|
291
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the review comment." })),
|
|
292
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the comment belongs to." })),
|
|
293
|
+
pull_request_number: Long.pipe(Schema.annotateKey({ description: "Number of the pull request the comment is on, in its repository." })),
|
|
294
|
+
pull_request_review_id: Schema.NullOr(Long).pipe(Schema.annotateKey({ description: "ID of the review the comment belongs to." })),
|
|
295
|
+
in_reply_to_id: Schema.optionalKey(Long).pipe(Schema.annotateKey({ description: "ID of the comment this one replies to." })),
|
|
296
|
+
user: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that wrote the comment." })),
|
|
297
|
+
author_association: Schema.String.pipe(Schema.annotateKey({ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR." })),
|
|
298
|
+
body: Schema.String.pipe(Schema.annotateKey({ description: "Comment in Markdown." })),
|
|
299
|
+
path: Schema.String.pipe(Schema.annotateKey({ description: "Path of the file the comment is on." })),
|
|
300
|
+
commit_id: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the comment is on now." })),
|
|
301
|
+
original_commit_id: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the comment was written on." })),
|
|
302
|
+
diff_hunk: Schema.String.pipe(Schema.annotateKey({ description: "Part of the diff the comment is on." })),
|
|
303
|
+
line: Schema.optionalKey(Schema.NullOr(Long)).pipe(Schema.annotateKey({ description: "Last line of the commented range in the current diff." })),
|
|
304
|
+
original_line: Schema.optionalKey(Schema.NullOr(Long)).pipe(Schema.annotateKey({ description: "Last line of the commented range in the original diff." })),
|
|
305
|
+
start_line: Schema.optionalKey(Schema.NullOr(Long)).pipe(Schema.annotateKey({ description: "First line of a multi-line comment." })),
|
|
306
|
+
side: Schema.optionalKey(Schema.NullOr(Schema.String)).pipe(Schema.annotateKey({ description: "Side of the diff: LEFT or RIGHT." })),
|
|
307
|
+
subject_type: Schema.optionalKey(Schema.String).pipe(Schema.annotateKey({ description: "What the comment is on: line or file." })),
|
|
308
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the comment was written." })),
|
|
309
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Comment on GitHub." })),
|
|
310
|
+
_deleted: deleted("review comment")
|
|
311
|
+
}).annotate({ description: "Review comments on pull request diffs." }).pipe(Iceberg.identifier("id"));
|
|
312
|
+
const PullRequestReviewCommentObjectSchema = PullRequestReviewCommentSchema.mapFields((fields) => ({
|
|
313
|
+
...Struct.omit(fields, [
|
|
314
|
+
"version",
|
|
315
|
+
"repository_id",
|
|
316
|
+
"pull_request_number",
|
|
317
|
+
"_deleted"
|
|
318
|
+
]),
|
|
319
|
+
updated_at: Timestamp,
|
|
320
|
+
pull_request_url: Schema.String.check(Schema.isPattern(/\/pulls\/\d+$/))
|
|
321
|
+
}));
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/schemas/pull-requests.ts
|
|
324
|
+
const branch = Schema.Struct({
|
|
325
|
+
ref: Schema.String.pipe(Schema.annotateKey({ description: "Branch name." })),
|
|
326
|
+
sha: Schema.String.pipe(Schema.annotateKey({ description: "Commit SHA." })),
|
|
327
|
+
label: Schema.String.pipe(Schema.annotateKey({ description: "Owner and branch, such as octocat:main." }))
|
|
328
|
+
});
|
|
329
|
+
const PullRequestSchema = Schema.Struct({
|
|
330
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique pull request ID. Not the ID of its issue." })),
|
|
331
|
+
version: version("pull request"),
|
|
332
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the pull request." })),
|
|
333
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the pull request belongs to." })),
|
|
334
|
+
number: Long.pipe(Schema.annotateKey({ description: "Pull request number in the repository." })),
|
|
335
|
+
title: Schema.String.pipe(Schema.annotateKey({ description: "Pull request title." })),
|
|
336
|
+
body: Text.pipe(Schema.annotateKey({ description: "Pull request description in Markdown. Empty when not set." })),
|
|
337
|
+
state: Schema.String.pipe(Schema.annotateKey({ description: "State: open or closed. Merged pull requests are closed." })),
|
|
338
|
+
draft: Schema.optionalKey(Schema.Boolean).pipe(Schema.annotateKey({ description: "Whether the pull request is a draft." })),
|
|
339
|
+
locked: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the conversation is locked." })),
|
|
340
|
+
user: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that opened the pull request." })),
|
|
341
|
+
author_association: Schema.String.pipe(Schema.annotateKey({ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR." })),
|
|
342
|
+
labels: Schema.Array(label).pipe(Schema.annotateKey({ description: "Labels on the pull request." })),
|
|
343
|
+
assignees: Schema.Array(user).pipe(Schema.annotateKey({ description: "Accounts assigned to the pull request." })),
|
|
344
|
+
requested_reviewers: Schema.Array(user).pipe(Schema.annotateKey({ description: "Accounts asked to review that have not reviewed yet." })),
|
|
345
|
+
milestone: Schema.NullOr(milestone).pipe(Schema.annotateKey({ description: "Milestone of the pull request." })),
|
|
346
|
+
head: branch.pipe(Schema.annotateKey({ description: "Branch the changes come from." })),
|
|
347
|
+
base: branch.pipe(Schema.annotateKey({ description: "Branch the changes merge into." })),
|
|
348
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the pull request was opened." })),
|
|
349
|
+
closed_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the pull request was closed. Null while it is open." })),
|
|
350
|
+
merged_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the pull request was merged." })),
|
|
351
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Pull request page on GitHub." }))
|
|
352
|
+
}).annotate({ description: "Pull requests in the installation's repositories." }).pipe(Iceberg.identifier("id"));
|
|
353
|
+
const PullRequestObjectSchema = PullRequestSchema.mapFields((fields) => ({
|
|
354
|
+
...Struct.omit(fields, [
|
|
355
|
+
"version",
|
|
356
|
+
"repository_id",
|
|
357
|
+
"labels",
|
|
358
|
+
"assignees",
|
|
359
|
+
"requested_reviewers"
|
|
360
|
+
]),
|
|
361
|
+
updated_at: Timestamp,
|
|
362
|
+
labels: optionalList(label),
|
|
363
|
+
assignees: optionalList(user),
|
|
364
|
+
requested_reviewers: optionalList(user)
|
|
365
|
+
}));
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/schemas/releases.ts
|
|
368
|
+
const asset = Schema.Struct({
|
|
369
|
+
id: Long.pipe(Schema.annotateKey({ description: "Asset ID." })),
|
|
370
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "File name." })),
|
|
371
|
+
label: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Label shown instead of the name." })),
|
|
372
|
+
state: Schema.String.pipe(Schema.annotateKey({ description: "State: uploaded or open." })),
|
|
373
|
+
content_type: Schema.String.pipe(Schema.annotateKey({ description: "Media type of the file." })),
|
|
374
|
+
size: Long.pipe(Schema.annotateKey({ description: "Size in bytes." })),
|
|
375
|
+
download_count: Long.pipe(Schema.annotateKey({ description: "Number of downloads when the release last changed." })),
|
|
376
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the asset was uploaded." })),
|
|
377
|
+
updated_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the asset last changed." })),
|
|
378
|
+
browser_download_url: Schema.String.pipe(Schema.annotateKey({ description: "Download link." }))
|
|
379
|
+
});
|
|
380
|
+
const ReleaseSchema = Schema.Struct({
|
|
381
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique release ID." })),
|
|
382
|
+
version: version("release"),
|
|
383
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the release." })),
|
|
384
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the release belongs to." })),
|
|
385
|
+
tag_name: Schema.String.pipe(Schema.annotateKey({ description: "Tag of the release." })),
|
|
386
|
+
target_commitish: Schema.String.pipe(Schema.annotateKey({ description: "Branch or commit the tag was created from." })),
|
|
387
|
+
name: Text.pipe(Schema.annotateKey({ description: "Release title. Empty when not set." })),
|
|
388
|
+
body: Text.pipe(Schema.annotateKey({ description: "Release notes in Markdown. Empty when not set." })),
|
|
389
|
+
draft: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the release is a draft." })),
|
|
390
|
+
prerelease: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the release is a pre-release." })),
|
|
391
|
+
author: user.pipe(Schema.annotateKey({ description: "Account that created the release." })),
|
|
392
|
+
assets: Schema.Array(asset).pipe(Schema.annotateKey({ description: "Files attached to the release." })),
|
|
393
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the release was created." })),
|
|
394
|
+
published_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the release was published." })),
|
|
395
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Release page on GitHub." })),
|
|
396
|
+
_deleted: deleted("release")
|
|
397
|
+
}).annotate({ description: "Releases in the installation's repositories, with their assets." }).pipe(Iceberg.identifier("id"));
|
|
398
|
+
const ReleaseObjectSchema = ReleaseSchema.mapFields((fields) => ({
|
|
399
|
+
...Struct.omit(fields, [
|
|
400
|
+
"version",
|
|
401
|
+
"repository_id",
|
|
402
|
+
"_deleted"
|
|
403
|
+
]),
|
|
404
|
+
updated_at: Schema.optionalKey(Schema.NullOr(Timestamp))
|
|
405
|
+
}));
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/schemas/repositories.ts
|
|
408
|
+
const RepositorySchema = Schema.Struct({
|
|
409
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique repository ID." })),
|
|
410
|
+
version: version("repository"),
|
|
411
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the repository." })),
|
|
412
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Repository name." })),
|
|
413
|
+
full_name: Schema.String.pipe(Schema.annotateKey({ description: "Owner and name, such as octo-org/octo-repo." })),
|
|
414
|
+
owner: user.pipe(Schema.annotateKey({ description: "Account that owns the repository." })),
|
|
415
|
+
private: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the repository is private." })),
|
|
416
|
+
visibility: Schema.optionalKey(Schema.String).pipe(Schema.annotateKey({ description: "Visibility: public, private, or internal." })),
|
|
417
|
+
description: Text.pipe(Schema.annotateKey({ description: "Repository description. Empty when not set." })),
|
|
418
|
+
fork: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the repository is a fork." })),
|
|
419
|
+
archived: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the repository is archived." })),
|
|
420
|
+
disabled: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether the repository is disabled." })),
|
|
421
|
+
default_branch: Schema.String.pipe(Schema.annotateKey({ description: "Default branch name." })),
|
|
422
|
+
language: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Main language GitHub detected." })),
|
|
423
|
+
topics: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Repository topics." })),
|
|
424
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Repository page on GitHub." })),
|
|
425
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the repository was created." })),
|
|
426
|
+
_deleted: deleted("repository")
|
|
427
|
+
}).annotate({ description: "Repositories the GitHub App installation can read." }).pipe(Iceberg.identifier("id"));
|
|
428
|
+
const RepositoryObjectSchema = RepositorySchema.mapFields((fields) => ({
|
|
429
|
+
...Struct.omit(fields, [
|
|
430
|
+
"version",
|
|
431
|
+
"topics",
|
|
432
|
+
"_deleted"
|
|
433
|
+
]),
|
|
434
|
+
updated_at: Timestamp,
|
|
435
|
+
topics: optionalList(Schema.String)
|
|
436
|
+
}));
|
|
437
|
+
const RepositoryRefSchema = Schema.Struct({
|
|
438
|
+
id: Schema.Number,
|
|
439
|
+
full_name: Schema.String,
|
|
440
|
+
has_issues: Schema.Boolean,
|
|
441
|
+
has_pull_requests: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))),
|
|
442
|
+
size: Schema.Number
|
|
443
|
+
});
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/schemas/workflow-runs.ts
|
|
446
|
+
const WorkflowRunSchema = Schema.Struct({
|
|
447
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique workflow run ID." })),
|
|
448
|
+
version: version("workflow run"),
|
|
449
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the run." })),
|
|
450
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the run belongs to." })),
|
|
451
|
+
workflow_id: Long.pipe(Schema.annotateKey({ description: "ID of the workflow." })),
|
|
452
|
+
name: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Workflow name." })),
|
|
453
|
+
display_title: Schema.String.pipe(Schema.annotateKey({ description: "Title of the run, such as the commit message." })),
|
|
454
|
+
path: Schema.String.pipe(Schema.annotateKey({ description: "Path of the workflow file." })),
|
|
455
|
+
event: Schema.String.pipe(Schema.annotateKey({ description: "Event that started the run, such as push." })),
|
|
456
|
+
status: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." })),
|
|
457
|
+
conclusion: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Result of a completed run, such as success or failure." })),
|
|
458
|
+
head_branch: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Branch the run is for." })),
|
|
459
|
+
head_sha: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the run is for." })),
|
|
460
|
+
run_number: Long.pipe(Schema.annotateKey({ description: "Run number of the workflow." })),
|
|
461
|
+
run_attempt: Schema.optionalKey(Long).pipe(Schema.annotateKey({ description: "Attempt number. Re-runs count up." })),
|
|
462
|
+
check_suite_id: Schema.optionalKey(Long).pipe(Schema.annotateKey({ description: "ID of the run's check suite." })),
|
|
463
|
+
actor: Schema.optionalKey(user).pipe(Schema.annotateKey({ description: "Account that started the first attempt." })),
|
|
464
|
+
triggering_actor: Schema.optionalKey(user).pipe(Schema.annotateKey({ description: "Account that started the latest attempt." })),
|
|
465
|
+
pull_requests: Schema.Array(Schema.Struct({
|
|
466
|
+
id: Long.pipe(Schema.annotateKey({ description: "Pull request ID." })),
|
|
467
|
+
number: Long.pipe(Schema.annotateKey({ description: "Pull request number." }))
|
|
468
|
+
})).pipe(Schema.annotateKey({ description: "Pull requests the run is for. Empty for forks." })),
|
|
469
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the run was created." })),
|
|
470
|
+
run_started_at: Schema.optionalKey(Timestamp).pipe(Schema.annotateKey({ description: "Time the latest attempt started." })),
|
|
471
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Run page on GitHub." }))
|
|
472
|
+
}).annotate({ description: "GitHub Actions workflow runs in the installation's repositories." }).pipe(Iceberg.identifier("id"));
|
|
473
|
+
const WorkflowRunObjectSchema = WorkflowRunSchema.mapFields((fields) => ({
|
|
474
|
+
...Struct.omit(fields, [
|
|
475
|
+
"version",
|
|
476
|
+
"repository_id",
|
|
477
|
+
"pull_requests"
|
|
478
|
+
]),
|
|
479
|
+
updated_at: Timestamp,
|
|
480
|
+
pull_requests: Schema.NullOr(fields.pull_requests)
|
|
481
|
+
}));
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/schemas/events.ts
|
|
484
|
+
/**
|
|
485
|
+
* The webhook route passes the payload on as it is. GitHub puts the event name
|
|
486
|
+
* in a header, so each resource decodes its own event.
|
|
487
|
+
*/
|
|
488
|
+
const WebhookPayloadSchema = Schema.Record(Schema.String, Schema.Unknown);
|
|
489
|
+
const InstallationEventSchema = Schema.Struct({ installation: Schema.optional(Schema.Struct({ id: Schema.Number })) });
|
|
490
|
+
/** For handlers that read the API again. */
|
|
491
|
+
const RepositoryNameSchema = Schema.Struct({
|
|
492
|
+
id: Long,
|
|
493
|
+
full_name: Schema.String
|
|
494
|
+
});
|
|
495
|
+
/** A transferred issue no longer exists in its old repository. */
|
|
496
|
+
const IssueRemovedEventSchema = Schema.Struct({
|
|
497
|
+
action: Schema.Literals(["deleted", "transferred"]),
|
|
498
|
+
issue: ObjectRefSchema
|
|
499
|
+
});
|
|
500
|
+
const IssuesEventSchema = Schema.Union([IssueRemovedEventSchema, Schema.Struct({
|
|
501
|
+
action: Schema.String,
|
|
502
|
+
repository: RepositoryNameSchema,
|
|
503
|
+
issue: IssueObjectSchema
|
|
504
|
+
})]);
|
|
505
|
+
const IssueCommentDeletedEventSchema = Schema.Struct({
|
|
506
|
+
action: Schema.Literal("deleted"),
|
|
507
|
+
comment: ObjectRefSchema
|
|
508
|
+
});
|
|
509
|
+
const IssueCommentEventSchema = Schema.Union([IssueCommentDeletedEventSchema, Schema.Struct({
|
|
510
|
+
action: Schema.String,
|
|
511
|
+
repository: ObjectRefSchema,
|
|
512
|
+
comment: IssueCommentObjectSchema
|
|
513
|
+
})]);
|
|
514
|
+
const PullRequestReviewCommentDeletedEventSchema = Schema.Struct({
|
|
515
|
+
action: Schema.Literal("deleted"),
|
|
516
|
+
comment: ObjectRefSchema
|
|
517
|
+
});
|
|
518
|
+
const PullRequestReviewCommentEventSchema = Schema.Union([PullRequestReviewCommentDeletedEventSchema, Schema.Struct({
|
|
519
|
+
action: Schema.String,
|
|
520
|
+
repository: ObjectRefSchema,
|
|
521
|
+
comment: PullRequestReviewCommentObjectSchema
|
|
522
|
+
})]);
|
|
523
|
+
const PullRequestEventSchema = Schema.Struct({
|
|
524
|
+
repository: RepositoryNameSchema,
|
|
525
|
+
pull_request: PullRequestObjectSchema
|
|
526
|
+
});
|
|
527
|
+
const RepositoryDeletedEventSchema = Schema.Struct({
|
|
528
|
+
action: Schema.Literal("deleted"),
|
|
529
|
+
repository: ObjectRefSchema
|
|
530
|
+
});
|
|
531
|
+
const RepositoryEventSchema = Schema.Union([RepositoryDeletedEventSchema, Schema.Struct({
|
|
532
|
+
action: Schema.String,
|
|
533
|
+
repository: RepositoryObjectSchema
|
|
534
|
+
})]);
|
|
535
|
+
const ReleaseDeletedEventSchema = Schema.Struct({
|
|
536
|
+
action: Schema.Literal("deleted"),
|
|
537
|
+
release: ObjectRefSchema
|
|
538
|
+
});
|
|
539
|
+
const ReleaseEventSchema = Schema.Union([ReleaseDeletedEventSchema, Schema.Struct({
|
|
540
|
+
action: Schema.String,
|
|
541
|
+
repository: ObjectRefSchema,
|
|
542
|
+
release: ReleaseObjectSchema
|
|
543
|
+
})]);
|
|
544
|
+
const WorkflowRunEventSchema = Schema.Struct({
|
|
545
|
+
repository: ObjectRefSchema,
|
|
546
|
+
workflow_run: WorkflowRunObjectSchema
|
|
547
|
+
});
|
|
548
|
+
const PushEventSchema = Schema.Struct({
|
|
549
|
+
ref: Schema.String,
|
|
550
|
+
before: Schema.String,
|
|
551
|
+
after: Schema.String,
|
|
552
|
+
repository: RepositoryNameSchema.mapFields(Struct.assign({ default_branch: Schema.String }))
|
|
553
|
+
});
|
|
554
|
+
const PullRequestReviewEventSchema = Schema.Struct({
|
|
555
|
+
repository: RepositoryNameSchema,
|
|
556
|
+
pull_request: Schema.Struct({ number: Long }),
|
|
557
|
+
review: ObjectRefSchema
|
|
558
|
+
});
|
|
559
|
+
const PullRequestCommitsEventSchema = Schema.Struct({
|
|
560
|
+
action: Schema.String,
|
|
561
|
+
changes: Schema.optionalKey(Schema.Struct({ base: Schema.optionalKey(Schema.Unknown) })),
|
|
562
|
+
repository: RepositoryNameSchema,
|
|
563
|
+
pull_request: Schema.Struct({
|
|
564
|
+
number: Long,
|
|
565
|
+
updated_at: Timestamp
|
|
566
|
+
})
|
|
567
|
+
});
|
|
568
|
+
const CheckRunEventSchema = Schema.Struct({
|
|
569
|
+
repository: RepositoryNameSchema,
|
|
570
|
+
check_run: ObjectRefSchema
|
|
571
|
+
});
|
|
572
|
+
const WorkflowJobEventSchema = Schema.Struct({
|
|
573
|
+
repository: RepositoryNameSchema,
|
|
574
|
+
workflow_job: ObjectRefSchema
|
|
575
|
+
});
|
|
576
|
+
/** The status payload is flat. */
|
|
577
|
+
const StatusEventSchema = Schema.Struct({
|
|
578
|
+
id: Long,
|
|
579
|
+
sha: Schema.String,
|
|
580
|
+
repository: RepositoryNameSchema
|
|
581
|
+
});
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/schemas/commits.ts
|
|
584
|
+
const gitActor = Schema.Struct({
|
|
585
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Name in the commit." })),
|
|
586
|
+
date: Timestamp.pipe(Schema.annotateKey({ description: "Time in the commit." }))
|
|
587
|
+
});
|
|
588
|
+
/**
|
|
589
|
+
* The GitHub account of a commit's author or committer. GitHub sends `null` or
|
|
590
|
+
* `{}` when the email matches no account, and both become `null`.
|
|
591
|
+
*/
|
|
592
|
+
const account = Schema.NullOr(Schema.Union([user, Schema.Struct({})])).pipe(Schema.decodeTo(Schema.NullOr(Schema.toType(user)), SchemaTransformation.transform({
|
|
593
|
+
decode: (value) => value !== null && "id" in value ? value : null,
|
|
594
|
+
encode: (value) => value
|
|
595
|
+
})));
|
|
596
|
+
const CommitSchema = Schema.Struct({
|
|
597
|
+
key: Schema.String.pipe(Schema.annotateKey({ description: "Repository ID and SHA, such as `1406396726:9eb3a7d`." })),
|
|
598
|
+
version: Schema.Date.pipe(Schema.annotateKey({ description: "Commit time. A commit never changes." })),
|
|
599
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the commit is in." })),
|
|
600
|
+
sha: Schema.String.pipe(Schema.annotateKey({ description: "Commit SHA." })),
|
|
601
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the commit." })),
|
|
602
|
+
message: Schema.String.pipe(Schema.annotateKey({ description: "Commit message." })),
|
|
603
|
+
author: gitActor.pipe(Schema.annotateKey({ description: "Author name and time from the commit." })),
|
|
604
|
+
committer: gitActor.pipe(Schema.annotateKey({ description: "Committer name and time from the commit." })),
|
|
605
|
+
author_user: account.pipe(Schema.annotateKey({ description: "GitHub account of the author." })),
|
|
606
|
+
committer_user: account.pipe(Schema.annotateKey({ description: "GitHub account of the committer." })),
|
|
607
|
+
parents: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "SHAs of the parent commits." })),
|
|
608
|
+
verified: Schema.Boolean.pipe(Schema.annotateKey({ description: "Whether GitHub verified the commit signature." })),
|
|
609
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Commit page on GitHub." }))
|
|
610
|
+
}).annotate({ description: "Commits on the default branch of the installation's repositories." }).pipe(Iceberg.identifier("key"));
|
|
611
|
+
const CommitObjectSchema = Schema.Struct({
|
|
612
|
+
sha: Schema.String,
|
|
613
|
+
node_id: Schema.String,
|
|
614
|
+
commit: Schema.Struct({
|
|
615
|
+
author: gitActor,
|
|
616
|
+
committer: gitActor,
|
|
617
|
+
message: Schema.String,
|
|
618
|
+
verification: Schema.optionalKey(Schema.Struct({ verified: Schema.Boolean }))
|
|
619
|
+
}),
|
|
620
|
+
author: account,
|
|
621
|
+
committer: account,
|
|
622
|
+
parents: Schema.Array(Schema.Struct({ sha: Schema.String })),
|
|
623
|
+
html_url: Schema.String
|
|
624
|
+
});
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/resources/shared.ts
|
|
627
|
+
const timestamps = {
|
|
628
|
+
createdAt: (item) => item.created_at,
|
|
629
|
+
updatedAt: (item) => item.updated_at
|
|
630
|
+
};
|
|
631
|
+
const changesInterval = Duration.minutes(15);
|
|
632
|
+
const maxChangesPagesPerRun = 20;
|
|
633
|
+
const changesOverlap = Duration.minutes(5);
|
|
634
|
+
const repositoryListTtl = Duration.minutes(5);
|
|
635
|
+
const installationRepositoriesPath = "/installation/repositories";
|
|
636
|
+
const repositoryPage = (item) => Schema.Struct({ repositories: Schema.Array(item) });
|
|
637
|
+
/** Some lists wrap their items, such as `{ total_count, workflow_runs: [...] }`. */
|
|
638
|
+
const wrappedList = (key, item) => Schema.Struct({ [key]: Schema.Array(item) }).pipe(Schema.decodeTo(Schema.Array(Schema.toType(item)), SchemaTransformation.transform({
|
|
639
|
+
decode: (page) => page[key],
|
|
640
|
+
encode: (items) => ({ [key]: items })
|
|
641
|
+
})));
|
|
642
|
+
const readPages = (client, pageSchema, url, keepGoing = () => true) => Stream.paginate(url, (current) => client.get(pageSchema, current).pipe(Effect.map((page) => [page.body, keepGoing(page.body) ? page.next : Option.none()]))).pipe(Stream.runCollect);
|
|
643
|
+
/** Every repository the installation can read, ordered by ID. */
|
|
644
|
+
const listRepositories = (client, schema) => readPages(client, wrappedList("repositories", schema), withParams(installationRepositoriesPath, { per_page: "100" })).pipe(Effect.map(Array.sort(Order.mapInput(Order.Number, (repository) => Number(repository.id)))));
|
|
645
|
+
const cachedRepositoryRefs = (client) => listRepositories(client, RepositoryRefSchema).pipe(Effect.cachedWithTTL((exit) => Exit.isSuccess(exit) ? repositoryListTtl : 0));
|
|
646
|
+
/**
|
|
647
|
+
* `since` is when the last run started, and `repositoryIds` are the
|
|
648
|
+
* repositories it read. Any other repository is new and is read from the start.
|
|
649
|
+
* `pending` is set while a run is split.
|
|
650
|
+
*/
|
|
651
|
+
const ChangesCursorSchema = Schema.Struct({
|
|
652
|
+
since: Schema.DateTimeUtcFromString,
|
|
653
|
+
repositoryIds: Schema.optional(Schema.Array(Schema.Number)),
|
|
654
|
+
pending: Schema.optional(Schema.Struct({
|
|
655
|
+
until: Schema.DateTimeUtcFromString,
|
|
656
|
+
repositoryIds: Schema.Array(Schema.Number),
|
|
657
|
+
repositoryId: Schema.Number,
|
|
658
|
+
url: Schema.optional(Schema.String)
|
|
659
|
+
}))
|
|
660
|
+
});
|
|
661
|
+
const ChangesCursorCodec = Schema.Union([Schema.fromJsonString(ChangesCursorSchema), Schema.DateTimeUtcFromString.pipe(Schema.decodeTo(Schema.toType(ChangesCursorSchema), SchemaTransformation.transform({
|
|
662
|
+
decode: (since) => ({ since }),
|
|
663
|
+
encode: (cursor) => cursor.since
|
|
664
|
+
})))]);
|
|
665
|
+
const cursorError = (cause) => new ConnectorError({
|
|
666
|
+
message: "Invalid GitHub cursor",
|
|
667
|
+
cause
|
|
668
|
+
});
|
|
669
|
+
const decodeChangesCursor = (value) => Schema.decodeUnknownEffect(ChangesCursorCodec)(value).pipe(Effect.mapError(cursorError));
|
|
670
|
+
const encodeChangesCursor = (cursor) => Schema.encodeEffect(Schema.fromJsonString(ChangesCursorSchema))(cursor).pipe(Effect.mapError(cursorError));
|
|
671
|
+
/** When to read a repository's changes from. None means from the start. */
|
|
672
|
+
const readFrom = (state, repositoryId) => state.repositoryIds === void 0 || state.repositoryIds.includes(repositoryId) ? Option.some(DateTime.toDateUtc(DateTime.subtractDuration(state.since, changesOverlap))) : Option.none();
|
|
673
|
+
const isSince = (time, date) => Option.match(time, {
|
|
674
|
+
onNone: () => true,
|
|
675
|
+
onSome: (since) => date.getTime() >= since.getTime()
|
|
676
|
+
});
|
|
677
|
+
const readChanges = (cursor, repositories, toRows) => Effect.gen(function* () {
|
|
678
|
+
const state = yield* decodeChangesCursor(cursor);
|
|
679
|
+
const runStart = yield* DateTime.now;
|
|
680
|
+
const listed = yield* repositories;
|
|
681
|
+
return {
|
|
682
|
+
rows: toRows(listed.map((repository) => ({
|
|
683
|
+
repository,
|
|
684
|
+
since: readFrom(state, Number(repository.id))
|
|
685
|
+
}))),
|
|
686
|
+
cursor: yield* encodeChangesCursor({
|
|
687
|
+
since: runStart,
|
|
688
|
+
repositoryIds: listed.map((repository) => Number(repository.id))
|
|
689
|
+
})
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
const rowsOf = (toRow) => (items, repository) => Effect.succeed(items.map((item) => toRow(item, BigInt(repository.id))));
|
|
693
|
+
const repoPath = (repository, path) => `/repos/${repository.full_name}/${path}`;
|
|
694
|
+
/**
|
|
695
|
+
* `rows` for a child resource, such as reviews of pull requests. Parents are
|
|
696
|
+
* read one at a time, as GitHub asks. `readAt` is the version for children
|
|
697
|
+
* without an update time.
|
|
698
|
+
*/
|
|
699
|
+
const childRows = (client, child) => (parents, repository) => Effect.gen(function* () {
|
|
700
|
+
const readAt = yield* DateTime.nowAsDate;
|
|
701
|
+
return (yield* Effect.forEach(parents, (parent) => readPages(client, child.pageSchema, withParams(repoPath(repository, child.path(parent)), {
|
|
702
|
+
...child.params,
|
|
703
|
+
per_page: "100"
|
|
704
|
+
})).pipe(Effect.map((children) => children.map((item) => child.toRow(item, parent, BigInt(repository.id), readAt)))))).flat();
|
|
705
|
+
});
|
|
706
|
+
/** Lists one item, then reads its rows, so a child resource checks its own call too. */
|
|
707
|
+
const checkRepoResource = (client, spec) => Effect.gen(function* () {
|
|
708
|
+
const page = yield* client.get(repositoryPage(RepositoryRefSchema), withParams(installationRepositoriesPath, { per_page: "100" }));
|
|
709
|
+
const repository = Array.findFirst(page.body.repositories, spec.enabled);
|
|
710
|
+
if (Option.isSome(repository)) {
|
|
711
|
+
const items = yield* client.get(spec.pageSchema, withParams(repoPath(repository.value, spec.path), {
|
|
712
|
+
...spec.params,
|
|
713
|
+
per_page: "1"
|
|
714
|
+
}));
|
|
715
|
+
yield* spec.rows(items.body.filter(spec.keep), repository.value);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
const BackfillCursorSchema = Schema.fromJsonString(Schema.Struct({
|
|
719
|
+
repositoryId: Schema.Number,
|
|
720
|
+
next: Schema.optional(Schema.String)
|
|
721
|
+
}));
|
|
722
|
+
const decodeBackfillCursor = (value) => Schema.decodeUnknownEffect(BackfillCursorSchema)(value).pipe(Effect.mapError(cursorError));
|
|
723
|
+
const encodeBackfillCursor = (cursor) => Schema.encodeEffect(BackfillCursorSchema)(cursor).pipe(Effect.mapError(cursorError));
|
|
724
|
+
const parseCutoff = (value) => DateTime.toDateUtc(DateTime.makeUnsafe(value));
|
|
725
|
+
/**
|
|
726
|
+
* Reads one repository at a time up to the cutoff, oldest first when the list
|
|
727
|
+
* can sort. The cursor keeps the `next` URL, because the issues list pages
|
|
728
|
+
* with a cursor.
|
|
729
|
+
*/
|
|
730
|
+
const backfillRepoResource = (client, repositories, spec) => Fetch.page({
|
|
731
|
+
pageCursor: Cursor.string(),
|
|
732
|
+
cutoff: Cursor.isoDateTime(),
|
|
733
|
+
fetch: ({ pageCursor, cutoff }) => Effect.gen(function* () {
|
|
734
|
+
const cutoffDate = parseCutoff(String(cutoff));
|
|
735
|
+
const listed = (yield* repositories).filter(spec.enabled);
|
|
736
|
+
const position = typeof pageCursor === "string" ? Option.some(yield* decodeBackfillCursor(pageCursor)) : Option.none();
|
|
737
|
+
const repository = Option.match(position, {
|
|
738
|
+
onNone: () => listed[0],
|
|
739
|
+
onSome: ({ repositoryId }) => listed.find((item) => item.id >= repositoryId)
|
|
740
|
+
});
|
|
741
|
+
if (repository === void 0) return {
|
|
742
|
+
rows: [],
|
|
743
|
+
hasMore: false
|
|
744
|
+
};
|
|
745
|
+
const url = Option.flatMap(position, ({ repositoryId, next }) => repositoryId === repository.id ? Option.fromNullishOr(next) : Option.none()).pipe(Option.getOrElse(() => withParams(repoPath(repository, spec.path), {
|
|
746
|
+
...spec.params,
|
|
747
|
+
...spec.order === "sorted" ? {
|
|
748
|
+
sort: "created",
|
|
749
|
+
direction: "asc"
|
|
750
|
+
} : {},
|
|
751
|
+
per_page: "100"
|
|
752
|
+
})));
|
|
753
|
+
const page = yield* client.get(spec.pageSchema, url);
|
|
754
|
+
const beforeCutoff = page.body.filter((item) => spec.createdAt(item).getTime() <= cutoffDate.getTime());
|
|
755
|
+
const rows = yield* spec.rows(beforeCutoff.filter(spec.keep), repository);
|
|
756
|
+
if (!(spec.order === "sorted" && beforeCutoff.length < page.body.length) && Option.isSome(page.next)) return {
|
|
757
|
+
rows,
|
|
758
|
+
nextPageCursor: yield* encodeBackfillCursor({
|
|
759
|
+
repositoryId: repository.id,
|
|
760
|
+
next: page.next.value
|
|
761
|
+
}),
|
|
762
|
+
hasMore: true
|
|
763
|
+
};
|
|
764
|
+
const following = listed.find((item) => item.id > repository.id);
|
|
765
|
+
if (following === void 0) return {
|
|
766
|
+
rows,
|
|
767
|
+
hasMore: false
|
|
768
|
+
};
|
|
769
|
+
return {
|
|
770
|
+
rows,
|
|
771
|
+
nextPageCursor: yield* encodeBackfillCursor({ repositoryId: following.id }),
|
|
772
|
+
hasMore: true
|
|
773
|
+
};
|
|
774
|
+
})
|
|
775
|
+
});
|
|
776
|
+
/**
|
|
777
|
+
* One repository's changes, newest first, so an edit during the read repeats a
|
|
778
|
+
* row instead of skipping one.
|
|
779
|
+
*/
|
|
780
|
+
const changesPages = (client, spec, repository, since, url) => {
|
|
781
|
+
const first = withParams(repoPath(repository, spec.path), {
|
|
782
|
+
...spec.params,
|
|
783
|
+
...spec.order === "sorted" ? {
|
|
784
|
+
sort: "updated",
|
|
785
|
+
direction: "desc"
|
|
786
|
+
} : {},
|
|
787
|
+
per_page: "100",
|
|
788
|
+
...spec.since && Option.isSome(since) ? { since: since.value.toISOString() } : {}
|
|
789
|
+
});
|
|
790
|
+
const isRecent = (item) => isSince(since, spec.updatedAt(item));
|
|
791
|
+
const { order, recentFor } = spec;
|
|
792
|
+
const createdFrom = Option.map(since, (date) => DateTime.toDateUtc(DateTime.subtractDuration(DateTime.fromDateUnsafe(date), recentFor ?? Duration.zero)));
|
|
793
|
+
const goOn = (items) => {
|
|
794
|
+
if (order === "sorted") return items.every(isRecent);
|
|
795
|
+
if (recentFor === void 0) return true;
|
|
796
|
+
return items.every((item) => isSince(createdFrom, spec.createdAt(item)));
|
|
797
|
+
};
|
|
798
|
+
return Stream.paginate(Option.getOrElse(url, () => first), (current) => Effect.gen(function* () {
|
|
799
|
+
const page = yield* client.get(spec.pageSchema, current);
|
|
800
|
+
const next = goOn(page.body) ? page.next : Option.none();
|
|
801
|
+
return [[{
|
|
802
|
+
repository,
|
|
803
|
+
rows: yield* spec.rows(page.body.filter((item) => isRecent(item) && spec.keep(item)), repository),
|
|
804
|
+
next
|
|
805
|
+
}], next];
|
|
806
|
+
}));
|
|
807
|
+
};
|
|
808
|
+
const changesRepoResource = (client, spec) => Fetch.changes({
|
|
809
|
+
cursor: Cursor.string(),
|
|
810
|
+
interval: changesInterval,
|
|
811
|
+
fetch: ({ cursor }) => Effect.gen(function* () {
|
|
812
|
+
const state = yield* decodeChangesCursor(String(cursor));
|
|
813
|
+
const listed = yield* listRepositories(client, RepositoryRefSchema);
|
|
814
|
+
const pending = state.pending;
|
|
815
|
+
const until = pending?.until ?? (yield* DateTime.now);
|
|
816
|
+
const repositoryIds = pending?.repositoryIds ?? listed.filter(spec.enabled).map((repository) => repository.id);
|
|
817
|
+
const repositories = listed.filter((repository) => spec.enabled(repository) && repositoryIds.includes(repository.id) && repository.id >= (pending?.repositoryId ?? 0));
|
|
818
|
+
const pages = yield* Stream.fromIterable(repositories).pipe(Stream.flatMap((repository) => changesPages(client, spec, repository, readFrom(state, repository.id), Option.fromNullishOr(pending?.repositoryId === repository.id ? pending.url : void 0))), Stream.take(maxChangesPagesPerRun), Stream.runCollect);
|
|
819
|
+
const rows = pages.flatMap((page) => page.rows);
|
|
820
|
+
const position = Option.flatMap(Array.last(pages), ({ repository, next }) => Option.match(next, {
|
|
821
|
+
onSome: (url) => Option.some({
|
|
822
|
+
repositoryId: repository.id,
|
|
823
|
+
url
|
|
824
|
+
}),
|
|
825
|
+
onNone: () => Array.findFirst(repositories, (item) => item.id > repository.id).pipe(Option.map((item) => ({ repositoryId: item.id })))
|
|
826
|
+
}));
|
|
827
|
+
if (pages.length === maxChangesPagesPerRun && Option.isSome(position)) return {
|
|
828
|
+
rows,
|
|
829
|
+
cursor: yield* encodeChangesCursor({
|
|
830
|
+
...state,
|
|
831
|
+
pending: {
|
|
832
|
+
until,
|
|
833
|
+
repositoryIds,
|
|
834
|
+
...position.value
|
|
835
|
+
}
|
|
836
|
+
}),
|
|
837
|
+
hasMore: true
|
|
838
|
+
};
|
|
839
|
+
return {
|
|
840
|
+
rows,
|
|
841
|
+
cursor: yield* encodeChangesCursor({
|
|
842
|
+
since: until,
|
|
843
|
+
repositoryIds
|
|
844
|
+
}),
|
|
845
|
+
hasMore: false
|
|
846
|
+
};
|
|
847
|
+
})
|
|
848
|
+
});
|
|
849
|
+
/** Schemas check that the URL ends in a number. */
|
|
850
|
+
const numberAtEnd = (url) => BigInt(url.slice(url.lastIndexOf("/") + 1));
|
|
851
|
+
const deleteRow = (id) => DateTime.nowAsDate.pipe(Effect.map((version) => ({
|
|
852
|
+
id,
|
|
853
|
+
version,
|
|
854
|
+
_deleted: true
|
|
855
|
+
})));
|
|
856
|
+
const decodeEvent = (schema, payload) => Schema.decodeUnknownEffect(schema)(payload).pipe(Effect.mapError((cause) => new ConnectorError({
|
|
857
|
+
message: "GitHub webhook payload does not decode",
|
|
858
|
+
cause
|
|
859
|
+
})));
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/resources/commits.ts
|
|
862
|
+
const toRow$12 = ({ sha, node_id, commit, author, committer, parents, html_url }, repository_id) => ({
|
|
863
|
+
key: `${repository_id}:${sha}`,
|
|
864
|
+
version: commit.committer.date,
|
|
865
|
+
repository_id,
|
|
866
|
+
sha,
|
|
867
|
+
node_id,
|
|
868
|
+
message: commit.message,
|
|
869
|
+
author: commit.author,
|
|
870
|
+
committer: commit.committer,
|
|
871
|
+
author_user: author,
|
|
872
|
+
committer_user: committer,
|
|
873
|
+
parents: parents.map((parent) => parent.sha),
|
|
874
|
+
verified: commit.verification?.verified ?? false,
|
|
875
|
+
html_url
|
|
876
|
+
});
|
|
877
|
+
const committedAt = (item) => item.commit.committer.date;
|
|
878
|
+
/** The list only has the default branch. */
|
|
879
|
+
const commitSpec = {
|
|
880
|
+
path: "commits",
|
|
881
|
+
pageSchema: Schema.Array(CommitObjectSchema),
|
|
882
|
+
params: {},
|
|
883
|
+
since: true,
|
|
884
|
+
order: "newest",
|
|
885
|
+
createdAt: committedAt,
|
|
886
|
+
updatedAt: committedAt,
|
|
887
|
+
keep: () => true,
|
|
888
|
+
enabled: (repository) => repository.size > 0,
|
|
889
|
+
rows: rowsOf(toRow$12)
|
|
890
|
+
};
|
|
891
|
+
const noCommit = /^0+$/;
|
|
892
|
+
const makeCommits = (client, repositories) => Resource.entity({
|
|
893
|
+
name: "commits",
|
|
894
|
+
rowSchema: CommitSchema,
|
|
895
|
+
version: "version",
|
|
896
|
+
check: checkRepoResource(client, commitSpec),
|
|
897
|
+
backfill: backfillRepoResource(client, repositories, commitSpec),
|
|
898
|
+
changes: changesRepoResource(client, commitSpec),
|
|
899
|
+
webhook: {
|
|
900
|
+
schema: WebhookPayloadSchema,
|
|
901
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
902
|
+
const { ref, before, after, repository } = yield* decodeEvent(PushEventSchema, payload);
|
|
903
|
+
if (ref !== `refs/heads/${repository.default_branch}` || noCommit.test(before) || noCommit.test(after)) return [];
|
|
904
|
+
return (yield* readPages(client, wrappedList("commits", CommitObjectSchema), withParams(`/repos/${repository.full_name}/compare/${before}...${after}`, { per_page: "100" }))).map((commit) => toRow$12(commit, repository.id));
|
|
905
|
+
})
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/resources/check-runs.ts
|
|
910
|
+
const toRow$11 = (checkRun, repository_id, version) => ({
|
|
911
|
+
...checkRun,
|
|
912
|
+
version,
|
|
913
|
+
repository_id
|
|
914
|
+
});
|
|
915
|
+
/** Check runs of default-branch commits. Webhooks also bring other branches. */
|
|
916
|
+
const checkRunSpec = (client) => ({
|
|
917
|
+
...commitSpec,
|
|
918
|
+
rows: childRows(client, {
|
|
919
|
+
path: (commit) => `commits/${commit.sha}/check-runs`,
|
|
920
|
+
pageSchema: wrappedList("check_runs", CheckRunObjectSchema),
|
|
921
|
+
params: { filter: "all" },
|
|
922
|
+
toRow: (checkRun, _commit, repositoryId, readAt) => toRow$11(checkRun, repositoryId, readAt)
|
|
923
|
+
})
|
|
924
|
+
});
|
|
925
|
+
const makeCheckRuns = (client, repositories) => {
|
|
926
|
+
const spec = checkRunSpec(client);
|
|
927
|
+
return Resource.entity({
|
|
928
|
+
name: "check_runs",
|
|
929
|
+
rowSchema: CheckRunSchema,
|
|
930
|
+
version: "version",
|
|
931
|
+
check: checkRepoResource(client, spec),
|
|
932
|
+
backfill: backfillRepoResource(client, repositories, spec),
|
|
933
|
+
changes: changesRepoResource(client, spec),
|
|
934
|
+
webhook: {
|
|
935
|
+
schema: WebhookPayloadSchema,
|
|
936
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
937
|
+
const { repository, check_run } = yield* decodeEvent(CheckRunEventSchema, payload);
|
|
938
|
+
const readAt = yield* DateTime.nowAsDate;
|
|
939
|
+
const page = yield* client.get(CheckRunObjectSchema, `/repos/${repository.full_name}/check-runs/${check_run.id}`);
|
|
940
|
+
return [toRow$11(page.body, repository.id, readAt)];
|
|
941
|
+
})
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
};
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/schemas/commit-statuses.ts
|
|
947
|
+
const CommitStatusSchema = Schema.Struct({
|
|
948
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique status ID. Each new status of a context gets a new ID." })),
|
|
949
|
+
version: version("status"),
|
|
950
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the status." })),
|
|
951
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the status belongs to." })),
|
|
952
|
+
sha: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the status is on." })),
|
|
953
|
+
state: Schema.String.pipe(Schema.annotateKey({ description: "State: error, failure, pending, or success." })),
|
|
954
|
+
context: Schema.String.pipe(Schema.annotateKey({ description: "Label that tells the status apart, such as ci/build." })),
|
|
955
|
+
description: Text.pipe(Schema.annotateKey({ description: "Short description. Empty when not set." })),
|
|
956
|
+
target_url: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Link the status points to." })),
|
|
957
|
+
creator: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that created the status." })),
|
|
958
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the status was created." }))
|
|
959
|
+
}).annotate({ description: "Commit statuses. Polled for default-branch commits; webhooks cover all branches." }).pipe(Iceberg.identifier("id"));
|
|
960
|
+
const CommitStatusObjectSchema = CommitStatusSchema.mapFields((fields) => ({
|
|
961
|
+
...Struct.omit(fields, [
|
|
962
|
+
"version",
|
|
963
|
+
"repository_id",
|
|
964
|
+
"sha"
|
|
965
|
+
]),
|
|
966
|
+
updated_at: Timestamp
|
|
967
|
+
}));
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/resources/commit-statuses.ts
|
|
970
|
+
const toRow$10 = ({ updated_at, ...rest }, sha, repository_id) => ({
|
|
971
|
+
...rest,
|
|
972
|
+
version: updated_at,
|
|
973
|
+
repository_id,
|
|
974
|
+
sha
|
|
975
|
+
});
|
|
976
|
+
/** Statuses of default-branch commits. Webhooks also bring other branches. */
|
|
977
|
+
const commitStatusSpec = (client) => ({
|
|
978
|
+
...commitSpec,
|
|
979
|
+
rows: childRows(client, {
|
|
980
|
+
path: (commit) => `commits/${commit.sha}/statuses`,
|
|
981
|
+
pageSchema: Schema.Array(CommitStatusObjectSchema),
|
|
982
|
+
toRow: (status, commit, repositoryId) => toRow$10(status, commit.sha, repositoryId)
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
const makeCommitStatuses = (client, repositories) => {
|
|
986
|
+
const spec = commitStatusSpec(client);
|
|
987
|
+
return Resource.entity({
|
|
988
|
+
name: "commit_statuses",
|
|
989
|
+
rowSchema: CommitStatusSchema,
|
|
990
|
+
version: "version",
|
|
991
|
+
check: checkRepoResource(client, spec),
|
|
992
|
+
backfill: backfillRepoResource(client, repositories, spec),
|
|
993
|
+
changes: changesRepoResource(client, spec),
|
|
994
|
+
webhook: {
|
|
995
|
+
schema: WebhookPayloadSchema,
|
|
996
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
997
|
+
const { id, sha, repository } = yield* decodeEvent(StatusEventSchema, payload);
|
|
998
|
+
const isStatus = (status) => status.id === id;
|
|
999
|
+
const status = yield* readPages(client, Schema.Array(CommitStatusObjectSchema), withParams(`/repos/${repository.full_name}/commits/${sha}/statuses`, { per_page: "100" }), (page) => !page.some(isStatus)).pipe(Effect.flatMap((statuses) => Effect.fromOption(Array.findFirst(statuses, isStatus), () => new ConnectorError({ message: `Commit status ${id} is not in the list` }))));
|
|
1000
|
+
return [toRow$10(status, sha, repository.id)];
|
|
1001
|
+
})
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/resources/issue-comments.ts
|
|
1007
|
+
const toRow$9 = ({ updated_at, issue_url, ...rest }, repository_id) => ({
|
|
1008
|
+
...rest,
|
|
1009
|
+
version: updated_at,
|
|
1010
|
+
repository_id,
|
|
1011
|
+
issue_number: numberAtEnd(issue_url)
|
|
1012
|
+
});
|
|
1013
|
+
const issueCommentSpec = {
|
|
1014
|
+
path: "issues/comments",
|
|
1015
|
+
pageSchema: Schema.Array(IssueCommentObjectSchema),
|
|
1016
|
+
params: {},
|
|
1017
|
+
since: true,
|
|
1018
|
+
order: "sorted",
|
|
1019
|
+
...timestamps,
|
|
1020
|
+
keep: () => true,
|
|
1021
|
+
enabled: (repository) => repository.has_issues || repository.has_pull_requests,
|
|
1022
|
+
rows: rowsOf(toRow$9)
|
|
1023
|
+
};
|
|
1024
|
+
const makeIssueComments = (client, repositories) => Resource.entity({
|
|
1025
|
+
name: "issue_comments",
|
|
1026
|
+
rowSchema: IssueCommentSchema,
|
|
1027
|
+
version: "version",
|
|
1028
|
+
check: checkRepoResource(client, issueCommentSpec),
|
|
1029
|
+
backfill: backfillRepoResource(client, repositories, issueCommentSpec),
|
|
1030
|
+
changes: changesRepoResource(client, issueCommentSpec),
|
|
1031
|
+
webhook: {
|
|
1032
|
+
schema: WebhookPayloadSchema,
|
|
1033
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1034
|
+
const event = yield* decodeEvent(IssueCommentEventSchema, payload);
|
|
1035
|
+
if (Schema.is(IssueCommentDeletedEventSchema)(event)) return [yield* deleteRow(event.comment.id)];
|
|
1036
|
+
return [toRow$9(event.comment, event.repository.id)];
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/resources/issues.ts
|
|
1042
|
+
const toRow$8 = ({ updated_at, labels, assignees, pull_request: _pullRequest, ...rest }, repository_id) => ({
|
|
1043
|
+
...rest,
|
|
1044
|
+
version: updated_at,
|
|
1045
|
+
repository_id,
|
|
1046
|
+
labels: labels ?? [],
|
|
1047
|
+
assignees: assignees ?? []
|
|
1048
|
+
});
|
|
1049
|
+
const hasLists$1 = ({ labels }) => labels !== void 0;
|
|
1050
|
+
const issueSpec = {
|
|
1051
|
+
path: "issues",
|
|
1052
|
+
pageSchema: Schema.Array(IssueObjectSchema),
|
|
1053
|
+
params: { state: "all" },
|
|
1054
|
+
since: true,
|
|
1055
|
+
order: "sorted",
|
|
1056
|
+
...timestamps,
|
|
1057
|
+
keep: (item) => item.pull_request === void 0,
|
|
1058
|
+
enabled: (repository) => repository.has_issues,
|
|
1059
|
+
rows: rowsOf(toRow$8)
|
|
1060
|
+
};
|
|
1061
|
+
const makeIssues = (client, repositories) => Resource.entity({
|
|
1062
|
+
name: "issues",
|
|
1063
|
+
rowSchema: IssueSchema,
|
|
1064
|
+
version: "version",
|
|
1065
|
+
check: checkRepoResource(client, issueSpec),
|
|
1066
|
+
backfill: backfillRepoResource(client, repositories, issueSpec),
|
|
1067
|
+
changes: changesRepoResource(client, issueSpec),
|
|
1068
|
+
webhook: {
|
|
1069
|
+
schema: WebhookPayloadSchema,
|
|
1070
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1071
|
+
const event = yield* decodeEvent(IssuesEventSchema, payload);
|
|
1072
|
+
if (Schema.is(IssueRemovedEventSchema)(event)) return [yield* deleteRow(event.issue.id)];
|
|
1073
|
+
const { issue, repository } = event;
|
|
1074
|
+
const full = hasLists$1(issue) ? issue : (yield* client.get(IssueObjectSchema, `/repos/${repository.full_name}/issues/${issue.number}`)).body;
|
|
1075
|
+
return [toRow$8(full, repository.id)];
|
|
1076
|
+
})
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
//#endregion
|
|
1080
|
+
//#region src/schemas/pull-request-commits.ts
|
|
1081
|
+
const PullRequestCommitSchema = Schema.Struct({
|
|
1082
|
+
key: Schema.String.pipe(Schema.annotateKey({ description: "Repository ID, pull request number, and SHA, such as `1406396726:3:9eb3a7d`." })),
|
|
1083
|
+
version: Schema.Date.pipe(Schema.annotateKey({ description: "Time GitHub last updated the pull request." })),
|
|
1084
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the pull request belongs to." })),
|
|
1085
|
+
pull_request_number: Long.pipe(Schema.annotateKey({ description: "Pull request number in the repository." })),
|
|
1086
|
+
sha: Schema.String.pipe(Schema.annotateKey({ description: "Commit SHA." })),
|
|
1087
|
+
message: Schema.String.pipe(Schema.annotateKey({ description: "Commit message." })),
|
|
1088
|
+
author: gitActor.pipe(Schema.annotateKey({ description: "Author name and time from the commit." })),
|
|
1089
|
+
committer: gitActor.pipe(Schema.annotateKey({ description: "Committer name and time from the commit." })),
|
|
1090
|
+
author_user: account.pipe(Schema.annotateKey({ description: "GitHub account of the author." })),
|
|
1091
|
+
committer_user: account.pipe(Schema.annotateKey({ description: "GitHub account of the committer." })),
|
|
1092
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Commit page on GitHub." }))
|
|
1093
|
+
}).annotate({ description: "Commits in pull requests, up to 250 per pull request. Commits removed by a force push stay." }).pipe(Iceberg.identifier("key"));
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/resources/pull-requests.ts
|
|
1096
|
+
const toRow$7 = ({ updated_at, labels, assignees, requested_reviewers, ...rest }, repository_id) => ({
|
|
1097
|
+
...rest,
|
|
1098
|
+
version: updated_at,
|
|
1099
|
+
repository_id,
|
|
1100
|
+
labels: labels ?? [],
|
|
1101
|
+
assignees: assignees ?? [],
|
|
1102
|
+
requested_reviewers: requested_reviewers ?? []
|
|
1103
|
+
});
|
|
1104
|
+
const hasLists = ({ assignees, requested_reviewers }) => assignees !== void 0 && requested_reviewers !== void 0;
|
|
1105
|
+
const pullRequestSpec = {
|
|
1106
|
+
path: "pulls",
|
|
1107
|
+
pageSchema: Schema.Array(PullRequestObjectSchema),
|
|
1108
|
+
params: { state: "all" },
|
|
1109
|
+
since: false,
|
|
1110
|
+
order: "sorted",
|
|
1111
|
+
...timestamps,
|
|
1112
|
+
keep: () => true,
|
|
1113
|
+
enabled: (repository) => repository.has_pull_requests,
|
|
1114
|
+
rows: rowsOf(toRow$7)
|
|
1115
|
+
};
|
|
1116
|
+
const makePullRequests = (client, repositories) => Resource.entity({
|
|
1117
|
+
name: "pull_requests",
|
|
1118
|
+
rowSchema: PullRequestSchema,
|
|
1119
|
+
version: "version",
|
|
1120
|
+
check: checkRepoResource(client, pullRequestSpec),
|
|
1121
|
+
backfill: backfillRepoResource(client, repositories, pullRequestSpec),
|
|
1122
|
+
changes: changesRepoResource(client, pullRequestSpec),
|
|
1123
|
+
webhook: {
|
|
1124
|
+
schema: WebhookPayloadSchema,
|
|
1125
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1126
|
+
const { pull_request, repository } = yield* decodeEvent(PullRequestEventSchema, payload);
|
|
1127
|
+
const full = hasLists(pull_request) ? pull_request : (yield* client.get(PullRequestObjectSchema, `/repos/${repository.full_name}/pulls/${pull_request.number}`)).body;
|
|
1128
|
+
return [toRow$7(full, repository.id)];
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/resources/pull-request-commits.ts
|
|
1134
|
+
const toRow$6 = ({ sha, commit, author, committer, html_url }, pullRequest, repository_id) => ({
|
|
1135
|
+
key: `${repository_id}:${pullRequest.number}:${sha}`,
|
|
1136
|
+
version: pullRequest.updated_at,
|
|
1137
|
+
repository_id,
|
|
1138
|
+
pull_request_number: pullRequest.number,
|
|
1139
|
+
sha,
|
|
1140
|
+
message: commit.message,
|
|
1141
|
+
author: commit.author,
|
|
1142
|
+
committer: commit.committer,
|
|
1143
|
+
author_user: author,
|
|
1144
|
+
committer_user: committer,
|
|
1145
|
+
html_url
|
|
1146
|
+
});
|
|
1147
|
+
const commitsPath = (pullRequest) => `pulls/${pullRequest.number}/commits`;
|
|
1148
|
+
const pullRequestCommitSpec = (client) => ({
|
|
1149
|
+
...pullRequestSpec,
|
|
1150
|
+
rows: childRows(client, {
|
|
1151
|
+
path: (pullRequest) => commitsPath(pullRequest),
|
|
1152
|
+
pageSchema: Schema.Array(CommitObjectSchema),
|
|
1153
|
+
toRow: toRow$6
|
|
1154
|
+
})
|
|
1155
|
+
});
|
|
1156
|
+
const commitActions = /* @__PURE__ */ new Set([
|
|
1157
|
+
"opened",
|
|
1158
|
+
"reopened",
|
|
1159
|
+
"synchronize"
|
|
1160
|
+
]);
|
|
1161
|
+
const makePullRequestCommits = (client, repositories) => {
|
|
1162
|
+
const spec = pullRequestCommitSpec(client);
|
|
1163
|
+
return Resource.entity({
|
|
1164
|
+
name: "pull_request_commits",
|
|
1165
|
+
rowSchema: PullRequestCommitSchema,
|
|
1166
|
+
version: "version",
|
|
1167
|
+
check: checkRepoResource(client, spec),
|
|
1168
|
+
backfill: backfillRepoResource(client, repositories, spec),
|
|
1169
|
+
changes: changesRepoResource(client, spec),
|
|
1170
|
+
webhook: {
|
|
1171
|
+
schema: WebhookPayloadSchema,
|
|
1172
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1173
|
+
const { action, changes, repository, pull_request } = yield* decodeEvent(PullRequestCommitsEventSchema, payload);
|
|
1174
|
+
if (!commitActions.has(action) && changes?.base === void 0) return [];
|
|
1175
|
+
return (yield* readPages(client, Schema.Array(CommitObjectSchema), withParams(`/repos/${repository.full_name}/${commitsPath(pull_request)}`, { per_page: "100" }))).map((commit) => toRow$6(commit, pull_request, repository.id));
|
|
1176
|
+
})
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/resources/pull-request-review-comments.ts
|
|
1182
|
+
const toRow$5 = ({ updated_at, pull_request_url, ...rest }, repository_id) => ({
|
|
1183
|
+
...rest,
|
|
1184
|
+
version: updated_at,
|
|
1185
|
+
repository_id,
|
|
1186
|
+
pull_request_number: numberAtEnd(pull_request_url)
|
|
1187
|
+
});
|
|
1188
|
+
const pullRequestReviewCommentSpec = {
|
|
1189
|
+
path: "pulls/comments",
|
|
1190
|
+
pageSchema: Schema.Array(PullRequestReviewCommentObjectSchema),
|
|
1191
|
+
params: {},
|
|
1192
|
+
since: true,
|
|
1193
|
+
order: "sorted",
|
|
1194
|
+
...timestamps,
|
|
1195
|
+
keep: () => true,
|
|
1196
|
+
enabled: (repository) => repository.has_pull_requests,
|
|
1197
|
+
rows: rowsOf(toRow$5)
|
|
1198
|
+
};
|
|
1199
|
+
const makePullRequestReviewComments = (client, repositories) => Resource.entity({
|
|
1200
|
+
name: "pull_request_review_comments",
|
|
1201
|
+
rowSchema: PullRequestReviewCommentSchema,
|
|
1202
|
+
version: "version",
|
|
1203
|
+
check: checkRepoResource(client, pullRequestReviewCommentSpec),
|
|
1204
|
+
backfill: backfillRepoResource(client, repositories, pullRequestReviewCommentSpec),
|
|
1205
|
+
changes: changesRepoResource(client, pullRequestReviewCommentSpec),
|
|
1206
|
+
webhook: {
|
|
1207
|
+
schema: WebhookPayloadSchema,
|
|
1208
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1209
|
+
const event = yield* decodeEvent(PullRequestReviewCommentEventSchema, payload);
|
|
1210
|
+
if (Schema.is(PullRequestReviewCommentDeletedEventSchema)(event)) return [yield* deleteRow(event.comment.id)];
|
|
1211
|
+
return [toRow$5(event.comment, event.repository.id)];
|
|
1212
|
+
})
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region src/schemas/pull-request-reviews.ts
|
|
1217
|
+
const PullRequestReviewSchema = Schema.Struct({
|
|
1218
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique review ID." })),
|
|
1219
|
+
version: readVersion("review"),
|
|
1220
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the review." })),
|
|
1221
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the review belongs to." })),
|
|
1222
|
+
pull_request_number: Long.pipe(Schema.annotateKey({ description: "Number of the pull request the review is on, in its repository." })),
|
|
1223
|
+
user: Schema.NullOr(user).pipe(Schema.annotateKey({ description: "Account that wrote the review." })),
|
|
1224
|
+
author_association: Schema.String.pipe(Schema.annotateKey({ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR." })),
|
|
1225
|
+
state: Schema.String.pipe(Schema.annotateKey({ description: "State, such as APPROVED, CHANGES_REQUESTED, COMMENTED, or DISMISSED." })),
|
|
1226
|
+
body: Schema.String.pipe(Schema.annotateKey({ description: "Review summary in Markdown." })),
|
|
1227
|
+
commit_id: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "SHA of the commit the review is on." })),
|
|
1228
|
+
submitted_at: Schema.optionalKey(Timestamp).pipe(Schema.annotateKey({ description: "Time the review was submitted." })),
|
|
1229
|
+
html_url: Schema.String.pipe(Schema.annotateKey({ description: "Review on GitHub." }))
|
|
1230
|
+
}).annotate({ description: "Reviews on pull requests. Pending reviews are only visible to their author." }).pipe(Iceberg.identifier("id"));
|
|
1231
|
+
const PullRequestReviewObjectSchema = PullRequestReviewSchema.mapFields((fields) => Struct.omit(fields, [
|
|
1232
|
+
"version",
|
|
1233
|
+
"repository_id",
|
|
1234
|
+
"pull_request_number"
|
|
1235
|
+
]));
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/resources/pull-request-reviews.ts
|
|
1238
|
+
const toRow$4 = (review, pull_request_number, repository_id, version) => ({
|
|
1239
|
+
...review,
|
|
1240
|
+
version,
|
|
1241
|
+
repository_id,
|
|
1242
|
+
pull_request_number
|
|
1243
|
+
});
|
|
1244
|
+
/** A review bumps its pull request's update time, so changes follow pull requests. */
|
|
1245
|
+
const reviewSpec = (client) => ({
|
|
1246
|
+
...pullRequestSpec,
|
|
1247
|
+
rows: childRows(client, {
|
|
1248
|
+
path: (pullRequest) => `pulls/${pullRequest.number}/reviews`,
|
|
1249
|
+
pageSchema: Schema.Array(PullRequestReviewObjectSchema),
|
|
1250
|
+
toRow: (review, pullRequest, repositoryId, readAt) => toRow$4(review, pullRequest.number, repositoryId, readAt)
|
|
1251
|
+
})
|
|
1252
|
+
});
|
|
1253
|
+
const makePullRequestReviews = (client, repositories) => {
|
|
1254
|
+
const spec = reviewSpec(client);
|
|
1255
|
+
return Resource.entity({
|
|
1256
|
+
name: "pull_request_reviews",
|
|
1257
|
+
rowSchema: PullRequestReviewSchema,
|
|
1258
|
+
version: "version",
|
|
1259
|
+
check: checkRepoResource(client, spec),
|
|
1260
|
+
backfill: backfillRepoResource(client, repositories, spec),
|
|
1261
|
+
changes: changesRepoResource(client, spec),
|
|
1262
|
+
webhook: {
|
|
1263
|
+
schema: WebhookPayloadSchema,
|
|
1264
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1265
|
+
const { repository, pull_request, review } = yield* decodeEvent(PullRequestReviewEventSchema, payload);
|
|
1266
|
+
const readAt = yield* DateTime.nowAsDate;
|
|
1267
|
+
const page = yield* client.get(PullRequestReviewObjectSchema, `/repos/${repository.full_name}/pulls/${pull_request.number}/reviews/${review.id}`);
|
|
1268
|
+
return [toRow$4(page.body, pull_request.number, repository.id, readAt)];
|
|
1269
|
+
})
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
};
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/resources/releases.ts
|
|
1275
|
+
const updatedAt = (release) => release.updated_at ?? release.created_at;
|
|
1276
|
+
const toRow$3 = (release, repository_id) => ({
|
|
1277
|
+
...Struct.omit(release, ["updated_at"]),
|
|
1278
|
+
version: updatedAt(release),
|
|
1279
|
+
repository_id
|
|
1280
|
+
});
|
|
1281
|
+
const releaseSpec = {
|
|
1282
|
+
path: "releases",
|
|
1283
|
+
pageSchema: Schema.Array(ReleaseObjectSchema),
|
|
1284
|
+
params: {},
|
|
1285
|
+
since: false,
|
|
1286
|
+
order: "newest",
|
|
1287
|
+
createdAt: (release) => release.created_at,
|
|
1288
|
+
updatedAt,
|
|
1289
|
+
keep: () => true,
|
|
1290
|
+
enabled: () => true,
|
|
1291
|
+
rows: rowsOf(toRow$3)
|
|
1292
|
+
};
|
|
1293
|
+
const makeReleases = (client, repositories) => Resource.entity({
|
|
1294
|
+
name: "releases",
|
|
1295
|
+
rowSchema: ReleaseSchema,
|
|
1296
|
+
version: "version",
|
|
1297
|
+
check: checkRepoResource(client, releaseSpec),
|
|
1298
|
+
backfill: backfillRepoResource(client, repositories, releaseSpec),
|
|
1299
|
+
changes: changesRepoResource(client, releaseSpec),
|
|
1300
|
+
webhook: {
|
|
1301
|
+
schema: WebhookPayloadSchema,
|
|
1302
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1303
|
+
const event = yield* decodeEvent(ReleaseEventSchema, payload);
|
|
1304
|
+
if (Schema.is(ReleaseDeletedEventSchema)(event)) return [yield* deleteRow(event.release.id)];
|
|
1305
|
+
return [toRow$3(event.release, event.repository.id)];
|
|
1306
|
+
})
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
//#endregion
|
|
1310
|
+
//#region src/resources/repositories.ts
|
|
1311
|
+
const toRow$2 = ({ updated_at, topics, ...rest }) => ({
|
|
1312
|
+
...rest,
|
|
1313
|
+
version: updated_at,
|
|
1314
|
+
topics: topics ?? [],
|
|
1315
|
+
_deleted: false
|
|
1316
|
+
});
|
|
1317
|
+
const makeRepositories = (client) => Resource.entity({
|
|
1318
|
+
name: "repositories",
|
|
1319
|
+
rowSchema: RepositorySchema,
|
|
1320
|
+
version: "version",
|
|
1321
|
+
check: client.get(repositoryPage(RepositoryObjectSchema), withParams(installationRepositoriesPath, { per_page: "1" })).pipe(Effect.asVoid),
|
|
1322
|
+
backfill: Fetch.page({
|
|
1323
|
+
pageCursor: Cursor.string(),
|
|
1324
|
+
cutoff: Cursor.isoDateTime(),
|
|
1325
|
+
fetch: ({ pageCursor, cutoff }) => Effect.gen(function* () {
|
|
1326
|
+
const cutoffDate = parseCutoff(String(cutoff));
|
|
1327
|
+
const page = yield* client.get(repositoryPage(RepositoryObjectSchema), typeof pageCursor === "string" ? pageCursor : withParams(installationRepositoriesPath, { per_page: "100" }));
|
|
1328
|
+
return {
|
|
1329
|
+
rows: page.body.repositories.filter((repository) => repository.created_at.getTime() <= cutoffDate.getTime()).map(toRow$2),
|
|
1330
|
+
nextPageCursor: Option.getOrUndefined(page.next),
|
|
1331
|
+
hasMore: Option.isSome(page.next)
|
|
1332
|
+
};
|
|
1333
|
+
})
|
|
1334
|
+
}),
|
|
1335
|
+
changes: Fetch.changes({
|
|
1336
|
+
cursor: Cursor.string(),
|
|
1337
|
+
interval: changesInterval,
|
|
1338
|
+
fetch: ({ cursor }) => readChanges(String(cursor), listRepositories(client, RepositoryObjectSchema), (repositories) => repositories.filter(({ repository, since }) => isSince(since, repository.updated_at)).map(({ repository }) => toRow$2(repository)))
|
|
1339
|
+
}),
|
|
1340
|
+
webhook: {
|
|
1341
|
+
schema: WebhookPayloadSchema,
|
|
1342
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1343
|
+
const event = yield* decodeEvent(RepositoryEventSchema, payload);
|
|
1344
|
+
if (Schema.is(RepositoryDeletedEventSchema)(event)) return [yield* deleteRow(event.repository.id)];
|
|
1345
|
+
const { repository } = event;
|
|
1346
|
+
const full = repository.topics !== void 0 ? repository : (yield* client.get(RepositoryObjectSchema, `/repos/${repository.full_name}`)).body;
|
|
1347
|
+
return [toRow$2(full)];
|
|
1348
|
+
})
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/schemas/workflow-jobs.ts
|
|
1353
|
+
const step = Schema.Struct({
|
|
1354
|
+
number: Long.pipe(Schema.annotateKey({ description: "Step number." })),
|
|
1355
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Step name." })),
|
|
1356
|
+
status: Schema.String.pipe(Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." })),
|
|
1357
|
+
conclusion: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Result of a completed step." })),
|
|
1358
|
+
started_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(Schema.annotateKey({ description: "Time the step started." })),
|
|
1359
|
+
completed_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(Schema.annotateKey({ description: "Time the step completed." }))
|
|
1360
|
+
});
|
|
1361
|
+
const WorkflowJobSchema = Schema.Struct({
|
|
1362
|
+
id: Long.pipe(Schema.annotateKey({ description: "Unique job ID." })),
|
|
1363
|
+
version: readVersion("job"),
|
|
1364
|
+
node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the job." })),
|
|
1365
|
+
repository_id: Long.pipe(Schema.annotateKey({ description: "ID of the repository the job belongs to." })),
|
|
1366
|
+
run_id: Long.pipe(Schema.annotateKey({ description: "ID of the workflow run." })),
|
|
1367
|
+
run_attempt: Schema.optionalKey(Long).pipe(Schema.annotateKey({ description: "Attempt of the run the job belongs to." })),
|
|
1368
|
+
workflow_name: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Workflow name." })),
|
|
1369
|
+
name: Schema.String.pipe(Schema.annotateKey({ description: "Job name." })),
|
|
1370
|
+
head_sha: Schema.String.pipe(Schema.annotateKey({ description: "SHA of the commit the job ran on." })),
|
|
1371
|
+
head_branch: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Branch the job ran on." })),
|
|
1372
|
+
status: Schema.String.pipe(Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." })),
|
|
1373
|
+
conclusion: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Result of a completed job, such as success or failure." })),
|
|
1374
|
+
created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the job was queued." })),
|
|
1375
|
+
started_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the job started." })),
|
|
1376
|
+
completed_at: Schema.NullOr(Timestamp).pipe(Schema.annotateKey({ description: "Time the job completed." })),
|
|
1377
|
+
runner_name: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Name of the runner." })),
|
|
1378
|
+
labels: Schema.Array(Schema.String).pipe(Schema.annotateKey({ description: "Runner labels the job asked for." })),
|
|
1379
|
+
steps: Schema.Array(step).pipe(Schema.annotateKey({ description: "Steps of the job." })),
|
|
1380
|
+
html_url: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Job page on GitHub." }))
|
|
1381
|
+
}).annotate({ description: "Jobs of GitHub Actions workflow runs. Each run attempt has its own jobs." }).pipe(Iceberg.identifier("id"));
|
|
1382
|
+
const WorkflowJobObjectSchema = WorkflowJobSchema.mapFields((fields) => ({
|
|
1383
|
+
...Struct.omit(fields, [
|
|
1384
|
+
"version",
|
|
1385
|
+
"repository_id",
|
|
1386
|
+
"steps"
|
|
1387
|
+
]),
|
|
1388
|
+
steps: Schema.optionalKey(Schema.Array(step))
|
|
1389
|
+
}));
|
|
1390
|
+
//#endregion
|
|
1391
|
+
//#region src/resources/workflow-runs.ts
|
|
1392
|
+
const toRow$1 = ({ updated_at, pull_requests, ...rest }, repository_id) => ({
|
|
1393
|
+
...rest,
|
|
1394
|
+
version: updated_at,
|
|
1395
|
+
repository_id,
|
|
1396
|
+
pull_requests: pull_requests ?? []
|
|
1397
|
+
});
|
|
1398
|
+
const workflowRunSpec = {
|
|
1399
|
+
path: "actions/runs",
|
|
1400
|
+
pageSchema: wrappedList("workflow_runs", WorkflowRunObjectSchema),
|
|
1401
|
+
params: {},
|
|
1402
|
+
since: false,
|
|
1403
|
+
order: "newest",
|
|
1404
|
+
recentFor: Duration.days(1),
|
|
1405
|
+
...timestamps,
|
|
1406
|
+
keep: () => true,
|
|
1407
|
+
enabled: () => true,
|
|
1408
|
+
rows: rowsOf(toRow$1)
|
|
1409
|
+
};
|
|
1410
|
+
const makeWorkflowRuns = (client, repositories) => Resource.entity({
|
|
1411
|
+
name: "workflow_runs",
|
|
1412
|
+
rowSchema: WorkflowRunSchema,
|
|
1413
|
+
version: "version",
|
|
1414
|
+
check: checkRepoResource(client, workflowRunSpec),
|
|
1415
|
+
backfill: backfillRepoResource(client, repositories, workflowRunSpec),
|
|
1416
|
+
changes: changesRepoResource(client, workflowRunSpec),
|
|
1417
|
+
webhook: {
|
|
1418
|
+
schema: WebhookPayloadSchema,
|
|
1419
|
+
handler: ({ payload }) => decodeEvent(WorkflowRunEventSchema, payload).pipe(Effect.map((event) => [toRow$1(event.workflow_run, event.repository.id)]))
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region src/resources/workflow-jobs.ts
|
|
1424
|
+
const toRow = ({ steps, ...rest }, repository_id, version) => ({
|
|
1425
|
+
...rest,
|
|
1426
|
+
version,
|
|
1427
|
+
repository_id,
|
|
1428
|
+
steps: steps ?? []
|
|
1429
|
+
});
|
|
1430
|
+
const workflowJobSpec = (client) => ({
|
|
1431
|
+
...workflowRunSpec,
|
|
1432
|
+
rows: childRows(client, {
|
|
1433
|
+
path: (run) => `actions/runs/${run.id}/jobs`,
|
|
1434
|
+
pageSchema: wrappedList("jobs", WorkflowJobObjectSchema),
|
|
1435
|
+
params: { filter: "all" },
|
|
1436
|
+
toRow: (job, _run, repositoryId, readAt) => toRow(job, repositoryId, readAt)
|
|
1437
|
+
})
|
|
1438
|
+
});
|
|
1439
|
+
const makeWorkflowJobs = (client, repositories) => {
|
|
1440
|
+
const spec = workflowJobSpec(client);
|
|
1441
|
+
return Resource.entity({
|
|
1442
|
+
name: "workflow_jobs",
|
|
1443
|
+
rowSchema: WorkflowJobSchema,
|
|
1444
|
+
version: "version",
|
|
1445
|
+
check: checkRepoResource(client, spec),
|
|
1446
|
+
backfill: backfillRepoResource(client, repositories, spec),
|
|
1447
|
+
changes: changesRepoResource(client, spec),
|
|
1448
|
+
webhook: {
|
|
1449
|
+
schema: WebhookPayloadSchema,
|
|
1450
|
+
handler: ({ payload }) => Effect.gen(function* () {
|
|
1451
|
+
const { repository, workflow_job } = yield* decodeEvent(WorkflowJobEventSchema, payload);
|
|
1452
|
+
const readAt = yield* DateTime.nowAsDate;
|
|
1453
|
+
const page = yield* client.get(WorkflowJobObjectSchema, `/repos/${repository.full_name}/actions/jobs/${workflow_job.id}`);
|
|
1454
|
+
return [toRow(page.body, repository.id, readAt)];
|
|
1455
|
+
})
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/webhook/signature.ts
|
|
1461
|
+
const prefix = "sha256=";
|
|
1462
|
+
const verifySignature = (options) => {
|
|
1463
|
+
const header = options.header ?? "";
|
|
1464
|
+
const expected = createHmac("sha256", options.secret).update(options.rawBody).digest();
|
|
1465
|
+
const provided = Buffer.from(header.slice(7), "hex");
|
|
1466
|
+
return header.startsWith(prefix) && provided.length === expected.length && timingSafeEqual(provided, expected) ? Effect.void : Effect.fail(new ConnectorError({ message: "Invalid GitHub webhook signature" }));
|
|
1467
|
+
};
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/webhook/route.ts
|
|
1470
|
+
const webhookPath = "/webhooks/github";
|
|
1471
|
+
const makeWebhookRoute = (options) => Webhook.route({
|
|
1472
|
+
path: webhookPath,
|
|
1473
|
+
ackMode: "after-enqueue",
|
|
1474
|
+
schema: WebhookPayloadSchema,
|
|
1475
|
+
handler: ({ request, rawBody, payload, to }) => Effect.gen(function* () {
|
|
1476
|
+
const verified = yield* verifySignature({
|
|
1477
|
+
rawBody,
|
|
1478
|
+
header: request.headers["x-hub-signature-256"],
|
|
1479
|
+
secret: options.secret
|
|
1480
|
+
}).pipe(Effect.result);
|
|
1481
|
+
if (Result.isFailure(verified)) return HttpServerResponse.jsonUnsafe({
|
|
1482
|
+
ok: false,
|
|
1483
|
+
error: verified.failure.message
|
|
1484
|
+
}, { status: 401 });
|
|
1485
|
+
const installationId = Schema.decodeUnknownOption(InstallationEventSchema)(payload).pipe(Option.flatMap(({ installation }) => Option.fromNullishOr(installation?.id)));
|
|
1486
|
+
if (Option.contains(installationId, options.installationId)) {
|
|
1487
|
+
const resources = options.targets.get(request.headers["x-github-event"] ?? "") ?? [];
|
|
1488
|
+
yield* Effect.forEach(resources, (resource) => to(resource, payload), { discard: true });
|
|
1489
|
+
}
|
|
1490
|
+
return HttpServerResponse.jsonUnsafe({ ok: true });
|
|
1491
|
+
})
|
|
1492
|
+
});
|
|
1493
|
+
//#endregion
|
|
1494
|
+
//#region src/connector.ts
|
|
1495
|
+
var connector_exports = /* @__PURE__ */ __exportAll({
|
|
1496
|
+
GitHubConfigDef: () => GitHubConfigDef,
|
|
1497
|
+
GitHubConnector: () => GitHubConnector,
|
|
1498
|
+
layer: () => layer,
|
|
1499
|
+
layerConfig: () => layerConfig,
|
|
1500
|
+
make: () => make,
|
|
1501
|
+
manifest: () => manifest
|
|
1502
|
+
});
|
|
1503
|
+
const make = Effect.fnUntraced(function* (config) {
|
|
1504
|
+
const client = yield* GitHubClient;
|
|
1505
|
+
const repositories = yield* cachedRepositoryRefs(client);
|
|
1506
|
+
const Repositories = makeRepositories(client);
|
|
1507
|
+
const Issues = makeIssues(client, repositories);
|
|
1508
|
+
const PullRequests = makePullRequests(client, repositories);
|
|
1509
|
+
const IssueComments = makeIssueComments(client, repositories);
|
|
1510
|
+
const PullRequestReviews = makePullRequestReviews(client, repositories);
|
|
1511
|
+
const PullRequestReviewComments = makePullRequestReviewComments(client, repositories);
|
|
1512
|
+
const PullRequestCommits = makePullRequestCommits(client, repositories);
|
|
1513
|
+
const Commits = makeCommits(client, repositories);
|
|
1514
|
+
const CheckRuns = makeCheckRuns(client, repositories);
|
|
1515
|
+
const CommitStatuses = makeCommitStatuses(client, repositories);
|
|
1516
|
+
const WorkflowRuns = makeWorkflowRuns(client, repositories);
|
|
1517
|
+
const WorkflowJobs = makeWorkflowJobs(client, repositories);
|
|
1518
|
+
const Releases = makeReleases(client, repositories);
|
|
1519
|
+
return Connector.define({
|
|
1520
|
+
name: "producer-github",
|
|
1521
|
+
title: "GitHub",
|
|
1522
|
+
resources: [
|
|
1523
|
+
Repositories,
|
|
1524
|
+
Issues,
|
|
1525
|
+
PullRequests,
|
|
1526
|
+
IssueComments,
|
|
1527
|
+
PullRequestReviews,
|
|
1528
|
+
PullRequestReviewComments,
|
|
1529
|
+
PullRequestCommits,
|
|
1530
|
+
Commits,
|
|
1531
|
+
CheckRuns,
|
|
1532
|
+
CommitStatuses,
|
|
1533
|
+
WorkflowRuns,
|
|
1534
|
+
WorkflowJobs,
|
|
1535
|
+
Releases
|
|
1536
|
+
],
|
|
1537
|
+
webhooks: [makeWebhookRoute({
|
|
1538
|
+
secret: Redacted.value(config.webhookSecret),
|
|
1539
|
+
installationId: config.installationId,
|
|
1540
|
+
targets: /* @__PURE__ */ new Map([
|
|
1541
|
+
["repository", [Repositories]],
|
|
1542
|
+
["issues", [Issues]],
|
|
1543
|
+
["pull_request", [PullRequests, PullRequestCommits]],
|
|
1544
|
+
["issue_comment", [IssueComments]],
|
|
1545
|
+
["pull_request_review", [PullRequestReviews]],
|
|
1546
|
+
["pull_request_review_comment", [PullRequestReviewComments]],
|
|
1547
|
+
["push", [Commits]],
|
|
1548
|
+
["check_run", [CheckRuns]],
|
|
1549
|
+
["status", [CommitStatuses]],
|
|
1550
|
+
["workflow_run", [WorkflowRuns]],
|
|
1551
|
+
["workflow_job", [WorkflowJobs]],
|
|
1552
|
+
["release", [Releases]]
|
|
1553
|
+
])
|
|
1554
|
+
})]
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
1557
|
+
var GitHubConnector = class extends Context.Service()("@useairfoil/producer-github/GitHubConnector") {};
|
|
1558
|
+
const layer = (config) => Layer.effect(GitHubConnector)(make(config).pipe(Effect.annotateLogs({ component: "github" }))).pipe(Layer.provide(layer$1(config)));
|
|
1559
|
+
const layerConfig = (config) => Layer.unwrap(Config.unwrap(config).pipe(Effect.map(layer)));
|
|
1560
|
+
//#endregion
|
|
1561
|
+
export { auth_exports as C, client_exports as S, IssueObjectSchema as _, WorkflowJobSchema as a, IssueCommentSchema as b, CommitStatusSchema as c, RepositoryObjectSchema as d, RepositorySchema as f, PullRequestReviewCommentSchema as g, PullRequestSchema as h, webhookPath as i, CommitSchema as l, PullRequestObjectSchema as m, connector_exports as n, PullRequestReviewSchema as o, ReleaseSchema as p, layerConfig as r, PullRequestCommitSchema as s, GitHubConnector as t, WorkflowRunSchema as u, IssueSchema as v, CheckRunSchema as x, IssueCommentObjectSchema as y };
|
|
1562
|
+
|
|
1563
|
+
//# sourceMappingURL=src-DKDUc00e.mjs.map
|