@useairfoil/producer-github 0.0.1 → 0.0.2

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.
Files changed (59) hide show
  1. package/README.md +116 -0
  2. package/dist/index.d.mts +1939 -0
  3. package/dist/index.mjs +3 -0
  4. package/dist/main.d.mts +1 -0
  5. package/dist/main.mjs +698 -0
  6. package/dist/main.mjs.map +1 -0
  7. package/dist/manifest.d.mts +56 -0
  8. package/dist/manifest.mjs +137 -0
  9. package/dist/manifest.mjs.map +1 -0
  10. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  11. package/dist/src-D6cTmVtF.mjs +1566 -0
  12. package/dist/src-D6cTmVtF.mjs.map +1 -0
  13. package/package.json +45 -7
  14. package/src/client/auth.ts +113 -0
  15. package/src/client/client.ts +192 -0
  16. package/src/client/constants.ts +4 -0
  17. package/src/connector.ts +97 -0
  18. package/src/create-table.ts +5 -0
  19. package/src/demo/changes.ts +420 -0
  20. package/src/demo/index.ts +59 -0
  21. package/src/demo/setup.ts +34 -0
  22. package/src/demo/shared.ts +101 -0
  23. package/src/index.ts +13 -0
  24. package/src/main.ts +22 -0
  25. package/src/manifest.ts +45 -0
  26. package/src/resources/check-runs.ts +72 -0
  27. package/src/resources/commit-statuses.ts +84 -0
  28. package/src/resources/commits.ts +101 -0
  29. package/src/resources/issue-comments.ts +75 -0
  30. package/src/resources/issues.ts +78 -0
  31. package/src/resources/pull-request-commits.ts +91 -0
  32. package/src/resources/pull-request-review-comments.ts +78 -0
  33. package/src/resources/pull-request-reviews.ts +76 -0
  34. package/src/resources/pull-requests.ts +79 -0
  35. package/src/resources/releases.ts +74 -0
  36. package/src/resources/repositories.ts +101 -0
  37. package/src/resources/shared.ts +480 -0
  38. package/src/resources/workflow-jobs.ts +73 -0
  39. package/src/resources/workflow-runs.ts +70 -0
  40. package/src/sandbox.ts +35 -0
  41. package/src/schemas/check-runs.ts +48 -0
  42. package/src/schemas/commit-statuses.ts +31 -0
  43. package/src/schemas/commits.ts +64 -0
  44. package/src/schemas/events.ts +139 -0
  45. package/src/schemas/issue-comments.ts +38 -0
  46. package/src/schemas/issues.ts +62 -0
  47. package/src/schemas/pull-request-commits.ts +28 -0
  48. package/src/schemas/pull-request-review-comments.ts +67 -0
  49. package/src/schemas/pull-request-reviews.ts +37 -0
  50. package/src/schemas/pull-requests.ts +75 -0
  51. package/src/schemas/releases.ts +53 -0
  52. package/src/schemas/repositories.ts +53 -0
  53. package/src/schemas/shared.ts +86 -0
  54. package/src/schemas/workflow-jobs.ts +54 -0
  55. package/src/schemas/workflow-runs.ts +55 -0
  56. package/src/start.ts +47 -0
  57. package/src/tables.ts +31 -0
  58. package/src/webhook/route.ts +46 -0
  59. package/src/webhook/signature.ts +20 -0
@@ -0,0 +1,53 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Effect, Schema, Struct } from "effect";
3
+
4
+ import { Long, Text, Timestamp, deleted, field, list, optionalList, user, version } from "./shared";
5
+
6
+ export const RepositorySchema = Schema.Struct({
7
+ id: Long.pipe(field(1, "Unique repository ID.")),
8
+ version: version(2, "repository"),
9
+ node_id: Schema.String.pipe(field(3, "GraphQL node ID of the repository.")),
10
+ name: Schema.String.pipe(field(4, "Repository name.")),
11
+ full_name: Schema.String.pipe(field(5, "Owner and name, such as octo-org/octo-repo.")),
12
+ owner: user(100).pipe(field(6, "Account that owns the repository.")),
13
+ private: Schema.Boolean.pipe(field(7, "Whether the repository is private.")),
14
+ visibility: Schema.optionalKey(Schema.String).pipe(
15
+ field(8, "Visibility: public, private, or internal."),
16
+ ),
17
+ description: Text.pipe(field(9, "Repository description. Empty when not set.")),
18
+ fork: Schema.Boolean.pipe(field(10, "Whether the repository is a fork.")),
19
+ archived: Schema.Boolean.pipe(field(11, "Whether the repository is archived.")),
20
+ disabled: Schema.Boolean.pipe(field(12, "Whether the repository is disabled.")),
21
+ default_branch: Schema.String.pipe(field(13, "Default branch name.")),
22
+ language: Schema.NullOr(Schema.String).pipe(field(14, "Main language GitHub detected.")),
23
+ topics: list(103, Schema.String).pipe(field(15, "Repository topics.")),
24
+ html_url: Schema.String.pipe(field(16, "Repository page on GitHub.")),
25
+ created_at: Timestamp.pipe(field(17, "Time the repository was created.")),
26
+ _deleted: deleted(18, "repository"),
27
+ })
28
+ .annotate({
29
+ description: "Repositories the GitHub App installation can read.",
30
+ })
31
+ .pipe(Iceberg.identifier("id"));
32
+
33
+ export type Repository = Schema.Schema.Type<typeof RepositorySchema>;
34
+
35
+ export const RepositoryObjectSchema = RepositorySchema.mapFields((fields) => ({
36
+ ...Struct.omit(fields, ["version", "topics", "_deleted"]),
37
+ updated_at: Timestamp,
38
+ topics: optionalList(Schema.String),
39
+ }));
40
+
41
+ export type RepositoryObject = Schema.Schema.Type<typeof RepositoryObjectSchema>;
42
+
43
+ export const RepositoryRefSchema = Schema.Struct({
44
+ id: Schema.Number,
45
+ full_name: Schema.String,
46
+ has_issues: Schema.Boolean,
47
+ // GitHub documents it as optional, defaulting to true.
48
+ has_pull_requests: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))),
49
+ // In KB. An empty repository has 0, and its git lists return 409.
50
+ size: Schema.Number,
51
+ });
52
+
53
+ export type RepositoryRef = Schema.Schema.Type<typeof RepositoryRefSchema>;
@@ -0,0 +1,86 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, SchemaTransformation } from "effect";
3
+
4
+ export const field = (fieldId: number, description: string) =>
5
+ Iceberg.field(fieldId, { description });
6
+
7
+ /** IDs and counts. Connector Kit maps whole numbers to Iceberg longs. */
8
+ export const Long = Schema.Int.pipe(
9
+ Schema.decodeTo(
10
+ Schema.BigInt,
11
+ SchemaTransformation.transform({
12
+ decode: (value) => BigInt(value),
13
+ encode: (value) => Number(value),
14
+ }),
15
+ ),
16
+ );
17
+
18
+ export const Timestamp = Schema.DateFromString;
19
+
20
+ /** Text GitHub can clear, such as a body. */
21
+ export const Text = Schema.NullOr(Schema.String).pipe(
22
+ Schema.decodeTo(
23
+ Schema.String,
24
+ SchemaTransformation.transform({
25
+ decode: (value) => value ?? "",
26
+ encode: (value) => value,
27
+ }),
28
+ ),
29
+ );
30
+
31
+ /** `null` becomes an empty list. A webhook can leave the list out. */
32
+ export const optionalList = <S extends Schema.Top>(item: S) =>
33
+ Schema.optionalKey(
34
+ Schema.NullOr(Schema.Array(item)).pipe(
35
+ Schema.decodeTo(
36
+ Schema.Array(Schema.toType(item)),
37
+ SchemaTransformation.transform({
38
+ decode: (value) => value ?? [],
39
+ encode: (value) => value,
40
+ }),
41
+ ),
42
+ ),
43
+ );
44
+
45
+ // Each helper takes the first field ID of its block.
46
+ export const user = (baseId: number) =>
47
+ Schema.Struct({
48
+ id: Long.pipe(field(baseId, "GitHub user ID.")),
49
+ login: Schema.String.pipe(field(baseId + 1, "GitHub username.")),
50
+ type: Schema.String.pipe(
51
+ field(baseId + 2, "Account type, such as User, Bot, or Organization."),
52
+ ),
53
+ });
54
+
55
+ export const label = (baseId: number) =>
56
+ Schema.Struct({
57
+ id: Long.pipe(field(baseId, "Label ID.")),
58
+ name: Schema.String.pipe(field(baseId + 1, "Label name.")),
59
+ });
60
+
61
+ export const milestone = (baseId: number) =>
62
+ Schema.Struct({
63
+ id: Long.pipe(field(baseId, "Milestone ID.")),
64
+ number: Long.pipe(field(baseId + 1, "Milestone number in the repository.")),
65
+ title: Schema.String.pipe(field(baseId + 2, "Milestone title.")),
66
+ });
67
+
68
+ /** A list column. The element ID is separate from the column's field ID. */
69
+ export const list = <S extends Schema.Top>(elementId: number, item: S) =>
70
+ Schema.Array(item.annotate({ fieldId: elementId }));
71
+
72
+ export const version = (fieldId: number, object: string) =>
73
+ Schema.Date.pipe(field(fieldId, `Time GitHub last updated the ${object}.`));
74
+
75
+ /** For objects without an update time. A newer read wins. */
76
+ export const readVersion = (fieldId: number, object: string) =>
77
+ Schema.Date.pipe(
78
+ field(fieldId, `Time the connector read the ${object}. GitHub has no update time for it.`),
79
+ );
80
+
81
+ export const deleted = (fieldId: number, object: string) =>
82
+ Schema.optional(Schema.Boolean).pipe(
83
+ field(fieldId, `Whether the ${object} was deleted in GitHub.`),
84
+ );
85
+
86
+ export const ObjectRefSchema = Schema.Struct({ id: Long });
@@ -0,0 +1,54 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Timestamp, field, list, readVersion } from "./shared";
5
+
6
+ const step = Schema.Struct({
7
+ number: Long.pipe(field(102, "Step number.")),
8
+ name: Schema.String.pipe(field(103, "Step name.")),
9
+ status: Schema.String.pipe(field(104, "Status, such as queued, in_progress, or completed.")),
10
+ conclusion: Schema.NullOr(Schema.String).pipe(field(105, "Result of a completed step.")),
11
+ started_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(
12
+ field(106, "Time the step started."),
13
+ ),
14
+ completed_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(
15
+ field(107, "Time the step completed."),
16
+ ),
17
+ });
18
+
19
+ export const WorkflowJobSchema = Schema.Struct({
20
+ id: Long.pipe(field(1, "Unique job ID.")),
21
+ version: readVersion(2, "job"),
22
+ node_id: Schema.String.pipe(field(3, "GraphQL node ID of the job.")),
23
+ repository_id: Long.pipe(field(4, "ID of the repository the job belongs to.")),
24
+ run_id: Long.pipe(field(5, "ID of the workflow run.")),
25
+ run_attempt: Schema.optionalKey(Long).pipe(field(6, "Attempt of the run the job belongs to.")),
26
+ workflow_name: Schema.NullOr(Schema.String).pipe(field(7, "Workflow name.")),
27
+ name: Schema.String.pipe(field(8, "Job name.")),
28
+ head_sha: Schema.String.pipe(field(9, "SHA of the commit the job ran on.")),
29
+ head_branch: Schema.NullOr(Schema.String).pipe(field(10, "Branch the job ran on.")),
30
+ status: Schema.String.pipe(field(11, "Status, such as queued, in_progress, or completed.")),
31
+ conclusion: Schema.NullOr(Schema.String).pipe(
32
+ field(12, "Result of a completed job, such as success or failure."),
33
+ ),
34
+ created_at: Timestamp.pipe(field(13, "Time the job was queued.")),
35
+ started_at: Timestamp.pipe(field(14, "Time the job started.")),
36
+ completed_at: Schema.NullOr(Timestamp).pipe(field(15, "Time the job completed.")),
37
+ runner_name: Schema.NullOr(Schema.String).pipe(field(16, "Name of the runner.")),
38
+ labels: list(100, Schema.String).pipe(field(17, "Runner labels the job asked for.")),
39
+ steps: list(101, step).pipe(field(18, "Steps of the job.")),
40
+ html_url: Schema.NullOr(Schema.String).pipe(field(19, "Job page on GitHub.")),
41
+ })
42
+ .annotate({
43
+ description: "Jobs of GitHub Actions workflow runs. Each run attempt has its own jobs.",
44
+ })
45
+ .pipe(Iceberg.identifier("id"));
46
+
47
+ export type WorkflowJob = Schema.Schema.Type<typeof WorkflowJobSchema>;
48
+
49
+ export const WorkflowJobObjectSchema = WorkflowJobSchema.mapFields((fields) => ({
50
+ ...Struct.omit(fields, ["version", "repository_id", "steps"]),
51
+ steps: Schema.optionalKey(Schema.Array(step)),
52
+ }));
53
+
54
+ export type WorkflowJobObject = Schema.Schema.Type<typeof WorkflowJobObjectSchema>;
@@ -0,0 +1,55 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Timestamp, field, list, user, version } from "./shared";
5
+
6
+ export const WorkflowRunSchema = Schema.Struct({
7
+ id: Long.pipe(field(1, "Unique workflow run ID.")),
8
+ version: version(2, "workflow run"),
9
+ node_id: Schema.String.pipe(field(3, "GraphQL node ID of the run.")),
10
+ repository_id: Long.pipe(field(4, "ID of the repository the run belongs to.")),
11
+ workflow_id: Long.pipe(field(5, "ID of the workflow.")),
12
+ name: Schema.NullOr(Schema.String).pipe(field(6, "Workflow name.")),
13
+ display_title: Schema.String.pipe(field(7, "Title of the run, such as the commit message.")),
14
+ path: Schema.String.pipe(field(8, "Path of the workflow file.")),
15
+ event: Schema.String.pipe(field(9, "Event that started the run, such as push.")),
16
+ status: Schema.NullOr(Schema.String).pipe(
17
+ field(10, "Status, such as queued, in_progress, or completed."),
18
+ ),
19
+ conclusion: Schema.NullOr(Schema.String).pipe(
20
+ field(11, "Result of a completed run, such as success or failure."),
21
+ ),
22
+ head_branch: Schema.NullOr(Schema.String).pipe(field(12, "Branch the run is for.")),
23
+ head_sha: Schema.String.pipe(field(13, "SHA of the commit the run is for.")),
24
+ run_number: Long.pipe(field(14, "Run number of the workflow.")),
25
+ run_attempt: Schema.optionalKey(Long).pipe(field(15, "Attempt number. Re-runs count up.")),
26
+ check_suite_id: Schema.optionalKey(Long).pipe(field(16, "ID of the run's check suite.")),
27
+ actor: Schema.optionalKey(user(100)).pipe(field(17, "Account that started the first attempt.")),
28
+ triggering_actor: Schema.optionalKey(user(103)).pipe(
29
+ field(18, "Account that started the latest attempt."),
30
+ ),
31
+ pull_requests: list(
32
+ 106,
33
+ Schema.Struct({
34
+ id: Long.pipe(field(107, "Pull request ID.")),
35
+ number: Long.pipe(field(108, "Pull request number.")),
36
+ }),
37
+ ).pipe(field(19, "Pull requests the run is for. Empty for forks.")),
38
+ created_at: Timestamp.pipe(field(20, "Time the run was created.")),
39
+ run_started_at: Schema.optionalKey(Timestamp).pipe(field(21, "Time the latest attempt started.")),
40
+ html_url: Schema.String.pipe(field(22, "Run page on GitHub.")),
41
+ })
42
+ .annotate({
43
+ description: "GitHub Actions workflow runs in the installation's repositories.",
44
+ })
45
+ .pipe(Iceberg.identifier("id"));
46
+
47
+ export type WorkflowRun = Schema.Schema.Type<typeof WorkflowRunSchema>;
48
+
49
+ export const WorkflowRunObjectSchema = WorkflowRunSchema.mapFields((fields) => ({
50
+ ...Struct.omit(fields, ["version", "repository_id", "pull_requests"]),
51
+ updated_at: Timestamp,
52
+ pull_requests: Schema.NullOr(fields.pull_requests),
53
+ }));
54
+
55
+ export type WorkflowRunObject = Schema.Schema.Type<typeof WorkflowRunObjectSchema>;
package/src/start.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { PgClient } from "@effect/sql-pg";
2
+ import {
3
+ ConnectorApp,
4
+ Ingestor,
5
+ RuntimeConfig,
6
+ StateStore,
7
+ Telemetry,
8
+ } from "@useairfoil/connector-kit";
9
+ import { Config, Effect, Layer, Logger, Schema } from "effect";
10
+ import { Command } from "effect/cli";
11
+
12
+ import { GitHubConnector } from "./index";
13
+
14
+ const ConnectorLayer = GitHubConnector.layerConfig(GitHubConnector.GitHubConfigDef.config);
15
+ const TelemetryLayer = Telemetry.layerOtlp();
16
+ const PostgresLayer = PgClient.layerConfig({
17
+ url: Config.schema(
18
+ Schema.Redacted(Schema.NonEmptyString),
19
+ RuntimeConfig.PlatformRuntimeKey.postgresConnectionString,
20
+ ),
21
+ maxConnections: Config.succeed(2),
22
+ connectTimeout: Config.succeed("5 seconds"),
23
+ applicationName: Config.succeed("airfoil-producer-github"),
24
+ });
25
+ const StateStoreLayer = StateStore.layerSql().pipe(Layer.provide(PostgresLayer));
26
+
27
+ export const startCommand = Command.make("start", {}, () =>
28
+ Effect.gen(function* () {
29
+ const port = yield* RuntimeConfig.httpPort;
30
+ const entrypoint = yield* GitHubConnector.GitHubConnector;
31
+
32
+ return yield* ConnectorApp.start(entrypoint, { port }).pipe(
33
+ Effect.provide(Ingestor.layerWingsConfig(entrypoint)),
34
+ );
35
+ }).pipe(
36
+ Effect.annotateLogs({ component: "github" }),
37
+ Effect.provide(
38
+ Layer.mergeAll(
39
+ StateStoreLayer,
40
+ ConnectorLayer,
41
+ Logger.layer([Logger.consolePretty()]),
42
+ TelemetryLayer,
43
+ ),
44
+ ),
45
+ Effect.provide(RuntimeConfig.layerHosted()),
46
+ ),
47
+ ).pipe(Command.withDescription("Run the production connector against Wings"));
package/src/tables.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Schema } from "effect";
2
+
3
+ import { CheckRunSchema } from "./schemas/check-runs";
4
+ import { CommitStatusSchema } from "./schemas/commit-statuses";
5
+ import { CommitSchema } from "./schemas/commits";
6
+ import { IssueCommentSchema } from "./schemas/issue-comments";
7
+ import { IssueSchema } from "./schemas/issues";
8
+ import { PullRequestCommitSchema } from "./schemas/pull-request-commits";
9
+ import { PullRequestReviewCommentSchema } from "./schemas/pull-request-review-comments";
10
+ import { PullRequestReviewSchema } from "./schemas/pull-request-reviews";
11
+ import { PullRequestSchema } from "./schemas/pull-requests";
12
+ import { ReleaseSchema } from "./schemas/releases";
13
+ import { RepositorySchema } from "./schemas/repositories";
14
+ import { WorkflowJobSchema } from "./schemas/workflow-jobs";
15
+ import { WorkflowRunSchema } from "./schemas/workflow-runs";
16
+
17
+ export const tableSchemas: Readonly<Record<string, Schema.Top>> = {
18
+ repositories: RepositorySchema,
19
+ issues: IssueSchema,
20
+ pull_requests: PullRequestSchema,
21
+ issue_comments: IssueCommentSchema,
22
+ pull_request_reviews: PullRequestReviewSchema,
23
+ pull_request_review_comments: PullRequestReviewCommentSchema,
24
+ pull_request_commits: PullRequestCommitSchema,
25
+ commits: CommitSchema,
26
+ check_runs: CheckRunSchema,
27
+ commit_statuses: CommitStatusSchema,
28
+ workflow_runs: WorkflowRunSchema,
29
+ workflow_jobs: WorkflowJobSchema,
30
+ releases: ReleaseSchema,
31
+ };
@@ -0,0 +1,46 @@
1
+ import { type ResourceDefinition, Webhook } from "@useairfoil/connector-kit";
2
+ import { Effect, Option, Result, Schema } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+
5
+ import { InstallationEventSchema, WebhookPayloadSchema } from "../schemas/events";
6
+ import { verifySignature } from "./signature";
7
+
8
+ export const webhookPath = "/webhooks/github";
9
+
10
+ export const makeWebhookRoute = (options: {
11
+ readonly secret: string;
12
+ readonly installationId: number;
13
+ /** Resources by `X-GitHub-Event` name. One event can feed several. */
14
+ readonly targets: ReadonlyMap<string, ReadonlyArray<ResourceDefinition>>;
15
+ }) =>
16
+ Webhook.route({
17
+ path: webhookPath,
18
+ ackMode: "after-enqueue",
19
+ schema: WebhookPayloadSchema,
20
+ handler: ({ request, rawBody, payload, to }) =>
21
+ Effect.gen(function* () {
22
+ const verified = yield* verifySignature({
23
+ rawBody,
24
+ header: request.headers["x-hub-signature-256"],
25
+ secret: options.secret,
26
+ }).pipe(Effect.result);
27
+ if (Result.isFailure(verified)) {
28
+ return HttpServerResponse.jsonUnsafe(
29
+ { ok: false, error: verified.failure.message },
30
+ { status: 401 },
31
+ );
32
+ }
33
+
34
+ // One app can be installed on several accounts, and all of their events
35
+ // come to the same URL. Only ours are stored. `ping` has no installation.
36
+ const installationId = Schema.decodeUnknownOption(InstallationEventSchema)(payload).pipe(
37
+ Option.flatMap(({ installation }) => Option.fromNullishOr(installation?.id)),
38
+ );
39
+ if (Option.contains(installationId, options.installationId)) {
40
+ const resources = options.targets.get(request.headers["x-github-event"] ?? "") ?? [];
41
+ yield* Effect.forEach(resources, (resource) => to(resource, payload), { discard: true });
42
+ }
43
+
44
+ return HttpServerResponse.jsonUnsafe({ ok: true });
45
+ }),
46
+ });
@@ -0,0 +1,20 @@
1
+ import { ConnectorError } from "@useairfoil/connector-kit";
2
+ import { Effect } from "effect";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
+
5
+ const prefix = "sha256=";
6
+
7
+ export const verifySignature = (options: {
8
+ readonly rawBody: Uint8Array;
9
+ readonly header: string | undefined;
10
+ readonly secret: string;
11
+ }): Effect.Effect<void, ConnectorError> => {
12
+ const header = options.header ?? "";
13
+ const expected = createHmac("sha256", options.secret).update(options.rawBody).digest();
14
+ const provided = Buffer.from(header.slice(prefix.length), "hex");
15
+ return header.startsWith(prefix) &&
16
+ provided.length === expected.length &&
17
+ timingSafeEqual(provided, expected)
18
+ ? Effect.void
19
+ : Effect.fail(new ConnectorError({ message: "Invalid GitHub webhook signature" }));
20
+ };