@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.
Files changed (60) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +116 -0
  3. package/dist/index.d.mts +1939 -0
  4. package/dist/index.mjs +3 -0
  5. package/dist/main.d.mts +1 -0
  6. package/dist/main.mjs +700 -0
  7. package/dist/main.mjs.map +1 -0
  8. package/dist/manifest.d.mts +56 -0
  9. package/dist/manifest.mjs +137 -0
  10. package/dist/manifest.mjs.map +1 -0
  11. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  12. package/dist/src-DKDUc00e.mjs +1563 -0
  13. package/dist/src-DKDUc00e.mjs.map +1 -0
  14. package/package.json +46 -8
  15. package/src/client/auth.ts +113 -0
  16. package/src/client/client.ts +192 -0
  17. package/src/client/constants.ts +4 -0
  18. package/src/connector.ts +97 -0
  19. package/src/create-table.ts +10 -0
  20. package/src/demo/changes.ts +420 -0
  21. package/src/demo/index.ts +59 -0
  22. package/src/demo/setup.ts +34 -0
  23. package/src/demo/shared.ts +101 -0
  24. package/src/index.ts +13 -0
  25. package/src/main.ts +28 -0
  26. package/src/manifest.ts +45 -0
  27. package/src/resources/check-runs.ts +72 -0
  28. package/src/resources/commit-statuses.ts +84 -0
  29. package/src/resources/commits.ts +101 -0
  30. package/src/resources/issue-comments.ts +75 -0
  31. package/src/resources/issues.ts +78 -0
  32. package/src/resources/pull-request-commits.ts +91 -0
  33. package/src/resources/pull-request-review-comments.ts +78 -0
  34. package/src/resources/pull-request-reviews.ts +76 -0
  35. package/src/resources/pull-requests.ts +79 -0
  36. package/src/resources/releases.ts +74 -0
  37. package/src/resources/repositories.ts +101 -0
  38. package/src/resources/shared.ts +480 -0
  39. package/src/resources/workflow-jobs.ts +73 -0
  40. package/src/resources/workflow-runs.ts +70 -0
  41. package/src/sandbox.ts +35 -0
  42. package/src/schemas/check-runs.ts +72 -0
  43. package/src/schemas/commit-statuses.ts +51 -0
  44. package/src/schemas/commits.ts +80 -0
  45. package/src/schemas/events.ts +139 -0
  46. package/src/schemas/issue-comments.ts +48 -0
  47. package/src/schemas/issues.ts +76 -0
  48. package/src/schemas/pull-request-commits.ts +42 -0
  49. package/src/schemas/pull-request-review-comments.ts +85 -0
  50. package/src/schemas/pull-request-reviews.ts +53 -0
  51. package/src/schemas/pull-requests.ts +95 -0
  52. package/src/schemas/releases.ts +70 -0
  53. package/src/schemas/repositories.ts +73 -0
  54. package/src/schemas/shared.ts +76 -0
  55. package/src/schemas/workflow-jobs.ts +78 -0
  56. package/src/schemas/workflow-runs.ts +72 -0
  57. package/src/start.ts +47 -0
  58. package/src/tables.ts +31 -0
  59. package/src/webhook/route.ts +46 -0
  60. package/src/webhook/signature.ts +20 -0
@@ -0,0 +1,95 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Text, Timestamp, label, milestone, optionalList, user, version } from "./shared";
5
+
6
+ const branch = Schema.Struct({
7
+ ref: Schema.String.pipe(Schema.annotateKey({ description: "Branch name." })),
8
+ sha: Schema.String.pipe(Schema.annotateKey({ description: "Commit SHA." })),
9
+ label: Schema.String.pipe(
10
+ Schema.annotateKey({ description: "Owner and branch, such as octocat:main." }),
11
+ ),
12
+ });
13
+
14
+ export const PullRequestSchema = Schema.Struct({
15
+ id: Long.pipe(
16
+ Schema.annotateKey({ description: "Unique pull request ID. Not the ID of its issue." }),
17
+ ),
18
+ version: version("pull request"),
19
+ node_id: Schema.String.pipe(
20
+ Schema.annotateKey({ description: "GraphQL node ID of the pull request." }),
21
+ ),
22
+ repository_id: Long.pipe(
23
+ Schema.annotateKey({ description: "ID of the repository the pull request belongs to." }),
24
+ ),
25
+ number: Long.pipe(Schema.annotateKey({ description: "Pull request number in the repository." })),
26
+ title: Schema.String.pipe(Schema.annotateKey({ description: "Pull request title." })),
27
+ body: Text.pipe(
28
+ Schema.annotateKey({
29
+ description: "Pull request description in Markdown. Empty when not set.",
30
+ }),
31
+ ),
32
+ state: Schema.String.pipe(
33
+ Schema.annotateKey({ description: "State: open or closed. Merged pull requests are closed." }),
34
+ ),
35
+ draft: Schema.optionalKey(Schema.Boolean).pipe(
36
+ Schema.annotateKey({ description: "Whether the pull request is a draft." }),
37
+ ),
38
+ locked: Schema.Boolean.pipe(
39
+ Schema.annotateKey({ description: "Whether the conversation is locked." }),
40
+ ),
41
+ user: Schema.NullOr(user).pipe(
42
+ Schema.annotateKey({ description: "Account that opened the pull request." }),
43
+ ),
44
+ author_association: Schema.String.pipe(
45
+ Schema.annotateKey({
46
+ description: "Author's relation to the repository, such as OWNER or CONTRIBUTOR.",
47
+ }),
48
+ ),
49
+ labels: Schema.Array(label).pipe(
50
+ Schema.annotateKey({ description: "Labels on the pull request." }),
51
+ ),
52
+ assignees: Schema.Array(user).pipe(
53
+ Schema.annotateKey({ description: "Accounts assigned to the pull request." }),
54
+ ),
55
+ requested_reviewers: Schema.Array(user).pipe(
56
+ Schema.annotateKey({ description: "Accounts asked to review that have not reviewed yet." }),
57
+ ),
58
+ milestone: Schema.NullOr(milestone).pipe(
59
+ Schema.annotateKey({ description: "Milestone of the pull request." }),
60
+ ),
61
+ head: branch.pipe(Schema.annotateKey({ description: "Branch the changes come from." })),
62
+ base: branch.pipe(Schema.annotateKey({ description: "Branch the changes merge into." })),
63
+ created_at: Timestamp.pipe(
64
+ Schema.annotateKey({ description: "Time the pull request was opened." }),
65
+ ),
66
+ closed_at: Schema.NullOr(Timestamp).pipe(
67
+ Schema.annotateKey({ description: "Time the pull request was closed. Null while it is open." }),
68
+ ),
69
+ merged_at: Schema.NullOr(Timestamp).pipe(
70
+ Schema.annotateKey({ description: "Time the pull request was merged." }),
71
+ ),
72
+ html_url: Schema.String.pipe(Schema.annotateKey({ description: "Pull request page on GitHub." })),
73
+ })
74
+ .annotate({
75
+ description: "Pull requests in the installation's repositories.",
76
+ })
77
+ .pipe(Iceberg.identifier("id"));
78
+
79
+ export type PullRequest = Schema.Schema.Type<typeof PullRequestSchema>;
80
+
81
+ export const PullRequestObjectSchema = PullRequestSchema.mapFields((fields) => ({
82
+ ...Struct.omit(fields, [
83
+ "version",
84
+ "repository_id",
85
+ "labels",
86
+ "assignees",
87
+ "requested_reviewers",
88
+ ]),
89
+ updated_at: Timestamp,
90
+ labels: optionalList(label),
91
+ assignees: optionalList(user),
92
+ requested_reviewers: optionalList(user),
93
+ }));
94
+
95
+ export type PullRequestObject = Schema.Schema.Type<typeof PullRequestObjectSchema>;
@@ -0,0 +1,70 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Text, Timestamp, deleted, user, version } from "./shared";
5
+
6
+ const asset = Schema.Struct({
7
+ id: Long.pipe(Schema.annotateKey({ description: "Asset ID." })),
8
+ name: Schema.String.pipe(Schema.annotateKey({ description: "File name." })),
9
+ label: Schema.NullOr(Schema.String).pipe(
10
+ Schema.annotateKey({ description: "Label shown instead of the name." }),
11
+ ),
12
+ state: Schema.String.pipe(Schema.annotateKey({ description: "State: uploaded or open." })),
13
+ content_type: Schema.String.pipe(Schema.annotateKey({ description: "Media type of the file." })),
14
+ size: Long.pipe(Schema.annotateKey({ description: "Size in bytes." })),
15
+ download_count: Long.pipe(
16
+ Schema.annotateKey({ description: "Number of downloads when the release last changed." }),
17
+ ),
18
+ created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the asset was uploaded." })),
19
+ updated_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the asset last changed." })),
20
+ browser_download_url: Schema.String.pipe(Schema.annotateKey({ description: "Download link." })),
21
+ });
22
+
23
+ export const ReleaseSchema = Schema.Struct({
24
+ id: Long.pipe(Schema.annotateKey({ description: "Unique release ID." })),
25
+ version: version("release"),
26
+ node_id: Schema.String.pipe(
27
+ Schema.annotateKey({ description: "GraphQL node ID of the release." }),
28
+ ),
29
+ repository_id: Long.pipe(
30
+ Schema.annotateKey({ description: "ID of the repository the release belongs to." }),
31
+ ),
32
+ tag_name: Schema.String.pipe(Schema.annotateKey({ description: "Tag of the release." })),
33
+ target_commitish: Schema.String.pipe(
34
+ Schema.annotateKey({ description: "Branch or commit the tag was created from." }),
35
+ ),
36
+ name: Text.pipe(Schema.annotateKey({ description: "Release title. Empty when not set." })),
37
+ body: Text.pipe(
38
+ Schema.annotateKey({ description: "Release notes in Markdown. Empty when not set." }),
39
+ ),
40
+ draft: Schema.Boolean.pipe(
41
+ Schema.annotateKey({ description: "Whether the release is a draft." }),
42
+ ),
43
+ prerelease: Schema.Boolean.pipe(
44
+ Schema.annotateKey({ description: "Whether the release is a pre-release." }),
45
+ ),
46
+ author: user.pipe(Schema.annotateKey({ description: "Account that created the release." })),
47
+ assets: Schema.Array(asset).pipe(
48
+ Schema.annotateKey({ description: "Files attached to the release." }),
49
+ ),
50
+ created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the release was created." })),
51
+ published_at: Schema.NullOr(Timestamp).pipe(
52
+ Schema.annotateKey({ description: "Time the release was published." }),
53
+ ),
54
+ html_url: Schema.String.pipe(Schema.annotateKey({ description: "Release page on GitHub." })),
55
+ _deleted: deleted("release"),
56
+ })
57
+ .annotate({
58
+ description: "Releases in the installation's repositories, with their assets.",
59
+ })
60
+ .pipe(Iceberg.identifier("id"));
61
+
62
+ export type Release = Schema.Schema.Type<typeof ReleaseSchema>;
63
+
64
+ export const ReleaseObjectSchema = ReleaseSchema.mapFields((fields) => ({
65
+ ...Struct.omit(fields, ["version", "repository_id", "_deleted"]),
66
+ // Not always set, according to the API schema.
67
+ updated_at: Schema.optionalKey(Schema.NullOr(Timestamp)),
68
+ }));
69
+
70
+ export type ReleaseObject = Schema.Schema.Type<typeof ReleaseObjectSchema>;
@@ -0,0 +1,73 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Effect, Schema, Struct } from "effect";
3
+
4
+ import { Long, Text, Timestamp, deleted, optionalList, user, version } from "./shared";
5
+
6
+ export const RepositorySchema = Schema.Struct({
7
+ id: Long.pipe(Schema.annotateKey({ description: "Unique repository ID." })),
8
+ version: version("repository"),
9
+ node_id: Schema.String.pipe(
10
+ Schema.annotateKey({ description: "GraphQL node ID of the repository." }),
11
+ ),
12
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Repository name." })),
13
+ full_name: Schema.String.pipe(
14
+ Schema.annotateKey({ description: "Owner and name, such as octo-org/octo-repo." }),
15
+ ),
16
+ owner: user.pipe(Schema.annotateKey({ description: "Account that owns the repository." })),
17
+ private: Schema.Boolean.pipe(
18
+ Schema.annotateKey({ description: "Whether the repository is private." }),
19
+ ),
20
+ visibility: Schema.optionalKey(Schema.String).pipe(
21
+ Schema.annotateKey({ description: "Visibility: public, private, or internal." }),
22
+ ),
23
+ description: Text.pipe(
24
+ Schema.annotateKey({ description: "Repository description. Empty when not set." }),
25
+ ),
26
+ fork: Schema.Boolean.pipe(
27
+ Schema.annotateKey({ description: "Whether the repository is a fork." }),
28
+ ),
29
+ archived: Schema.Boolean.pipe(
30
+ Schema.annotateKey({ description: "Whether the repository is archived." }),
31
+ ),
32
+ disabled: Schema.Boolean.pipe(
33
+ Schema.annotateKey({ description: "Whether the repository is disabled." }),
34
+ ),
35
+ default_branch: Schema.String.pipe(Schema.annotateKey({ description: "Default branch name." })),
36
+ language: Schema.NullOr(Schema.String).pipe(
37
+ Schema.annotateKey({ description: "Main language GitHub detected." }),
38
+ ),
39
+ topics: Schema.Array(Schema.String).pipe(
40
+ Schema.annotateKey({ description: "Repository topics." }),
41
+ ),
42
+ html_url: Schema.String.pipe(Schema.annotateKey({ description: "Repository page on GitHub." })),
43
+ created_at: Timestamp.pipe(
44
+ Schema.annotateKey({ description: "Time the repository was created." }),
45
+ ),
46
+ _deleted: deleted("repository"),
47
+ })
48
+ .annotate({
49
+ description: "Repositories the GitHub App installation can read.",
50
+ })
51
+ .pipe(Iceberg.identifier("id"));
52
+
53
+ export type Repository = Schema.Schema.Type<typeof RepositorySchema>;
54
+
55
+ export const RepositoryObjectSchema = RepositorySchema.mapFields((fields) => ({
56
+ ...Struct.omit(fields, ["version", "topics", "_deleted"]),
57
+ updated_at: Timestamp,
58
+ topics: optionalList(Schema.String),
59
+ }));
60
+
61
+ export type RepositoryObject = Schema.Schema.Type<typeof RepositoryObjectSchema>;
62
+
63
+ export const RepositoryRefSchema = Schema.Struct({
64
+ id: Schema.Number,
65
+ full_name: Schema.String,
66
+ has_issues: Schema.Boolean,
67
+ // GitHub documents it as optional, defaulting to true.
68
+ has_pull_requests: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))),
69
+ // In KB. An empty repository has 0, and its git lists return 409.
70
+ size: Schema.Number,
71
+ });
72
+
73
+ export type RepositoryRef = Schema.Schema.Type<typeof RepositoryRefSchema>;
@@ -0,0 +1,76 @@
1
+ import { Schema, SchemaTransformation } from "effect";
2
+
3
+ /** IDs and counts. Connector Kit maps whole numbers to Iceberg longs. */
4
+ export const Long = Schema.Int.pipe(
5
+ Schema.decodeTo(
6
+ Schema.BigInt,
7
+ SchemaTransformation.transform({
8
+ decode: (value) => BigInt(value),
9
+ encode: (value) => Number(value),
10
+ }),
11
+ ),
12
+ );
13
+
14
+ export const Timestamp = Schema.DateFromString;
15
+
16
+ /** Text GitHub can clear, such as a body. */
17
+ export const Text = Schema.NullOr(Schema.String).pipe(
18
+ Schema.decodeTo(
19
+ Schema.String,
20
+ SchemaTransformation.transform({
21
+ decode: (value) => value ?? "",
22
+ encode: (value) => value,
23
+ }),
24
+ ),
25
+ );
26
+
27
+ /** `null` becomes an empty list. A webhook can leave the list out. */
28
+ export const optionalList = <S extends Schema.Top>(item: S) =>
29
+ Schema.optionalKey(
30
+ Schema.NullOr(Schema.Array(item)).pipe(
31
+ Schema.decodeTo(
32
+ Schema.Array(Schema.toType(item)),
33
+ SchemaTransformation.transform({
34
+ decode: (value) => value ?? [],
35
+ encode: (value) => value,
36
+ }),
37
+ ),
38
+ ),
39
+ );
40
+
41
+ export const user = Schema.Struct({
42
+ id: Long.pipe(Schema.annotateKey({ description: "GitHub user ID." })),
43
+ login: Schema.String.pipe(Schema.annotateKey({ description: "GitHub username." })),
44
+ type: Schema.String.pipe(
45
+ Schema.annotateKey({ description: "Account type, such as User, Bot, or Organization." }),
46
+ ),
47
+ });
48
+
49
+ export const label = Schema.Struct({
50
+ id: Long.pipe(Schema.annotateKey({ description: "Label ID." })),
51
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Label name." })),
52
+ });
53
+
54
+ export const milestone = Schema.Struct({
55
+ id: Long.pipe(Schema.annotateKey({ description: "Milestone ID." })),
56
+ number: Long.pipe(Schema.annotateKey({ description: "Milestone number in the repository." })),
57
+ title: Schema.String.pipe(Schema.annotateKey({ description: "Milestone title." })),
58
+ });
59
+
60
+ export const version = (object: string) =>
61
+ Schema.Date.pipe(Schema.annotateKey({ description: `Time GitHub last updated the ${object}.` }));
62
+
63
+ /** For objects without an update time. A newer read wins. */
64
+ export const readVersion = (object: string) =>
65
+ Schema.Date.pipe(
66
+ Schema.annotateKey({
67
+ description: `Time the connector read the ${object}. GitHub has no update time for it.`,
68
+ }),
69
+ );
70
+
71
+ export const deleted = (object: string) =>
72
+ Schema.optional(Schema.Boolean).pipe(
73
+ Schema.annotateKey({ description: `Whether the ${object} was deleted in GitHub.` }),
74
+ );
75
+
76
+ export const ObjectRefSchema = Schema.Struct({ id: Long });
@@ -0,0 +1,78 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Timestamp, readVersion } from "./shared";
5
+
6
+ const step = Schema.Struct({
7
+ number: Long.pipe(Schema.annotateKey({ description: "Step number." })),
8
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Step name." })),
9
+ status: Schema.String.pipe(
10
+ Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." }),
11
+ ),
12
+ conclusion: Schema.NullOr(Schema.String).pipe(
13
+ Schema.annotateKey({ description: "Result of a completed step." }),
14
+ ),
15
+ started_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(
16
+ Schema.annotateKey({ description: "Time the step started." }),
17
+ ),
18
+ completed_at: Schema.optionalKey(Schema.NullOr(Timestamp)).pipe(
19
+ Schema.annotateKey({ description: "Time the step completed." }),
20
+ ),
21
+ });
22
+
23
+ export const WorkflowJobSchema = Schema.Struct({
24
+ id: Long.pipe(Schema.annotateKey({ description: "Unique job ID." })),
25
+ version: readVersion("job"),
26
+ node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the job." })),
27
+ repository_id: Long.pipe(
28
+ Schema.annotateKey({ description: "ID of the repository the job belongs to." }),
29
+ ),
30
+ run_id: Long.pipe(Schema.annotateKey({ description: "ID of the workflow run." })),
31
+ run_attempt: Schema.optionalKey(Long).pipe(
32
+ Schema.annotateKey({ description: "Attempt of the run the job belongs to." }),
33
+ ),
34
+ workflow_name: Schema.NullOr(Schema.String).pipe(
35
+ Schema.annotateKey({ description: "Workflow name." }),
36
+ ),
37
+ name: Schema.String.pipe(Schema.annotateKey({ description: "Job name." })),
38
+ head_sha: Schema.String.pipe(
39
+ Schema.annotateKey({ description: "SHA of the commit the job ran on." }),
40
+ ),
41
+ head_branch: Schema.NullOr(Schema.String).pipe(
42
+ Schema.annotateKey({ description: "Branch the job ran on." }),
43
+ ),
44
+ status: Schema.String.pipe(
45
+ Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." }),
46
+ ),
47
+ conclusion: Schema.NullOr(Schema.String).pipe(
48
+ Schema.annotateKey({ description: "Result of a completed job, such as success or failure." }),
49
+ ),
50
+ created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the job was queued." })),
51
+ started_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the job started." })),
52
+ completed_at: Schema.NullOr(Timestamp).pipe(
53
+ Schema.annotateKey({ description: "Time the job completed." }),
54
+ ),
55
+ runner_name: Schema.NullOr(Schema.String).pipe(
56
+ Schema.annotateKey({ description: "Name of the runner." }),
57
+ ),
58
+ labels: Schema.Array(Schema.String).pipe(
59
+ Schema.annotateKey({ description: "Runner labels the job asked for." }),
60
+ ),
61
+ steps: Schema.Array(step).pipe(Schema.annotateKey({ description: "Steps of the job." })),
62
+ html_url: Schema.NullOr(Schema.String).pipe(
63
+ Schema.annotateKey({ description: "Job page on GitHub." }),
64
+ ),
65
+ })
66
+ .annotate({
67
+ description: "Jobs of GitHub Actions workflow runs. Each run attempt has its own jobs.",
68
+ })
69
+ .pipe(Iceberg.identifier("id"));
70
+
71
+ export type WorkflowJob = Schema.Schema.Type<typeof WorkflowJobSchema>;
72
+
73
+ export const WorkflowJobObjectSchema = WorkflowJobSchema.mapFields((fields) => ({
74
+ ...Struct.omit(fields, ["version", "repository_id", "steps"]),
75
+ steps: Schema.optionalKey(Schema.Array(step)),
76
+ }));
77
+
78
+ export type WorkflowJobObject = Schema.Schema.Type<typeof WorkflowJobObjectSchema>;
@@ -0,0 +1,72 @@
1
+ import { Iceberg } from "@useairfoil/connector-kit";
2
+ import { Schema, Struct } from "effect";
3
+
4
+ import { Long, Timestamp, user, version } from "./shared";
5
+
6
+ export const WorkflowRunSchema = Schema.Struct({
7
+ id: Long.pipe(Schema.annotateKey({ description: "Unique workflow run ID." })),
8
+ version: version("workflow run"),
9
+ node_id: Schema.String.pipe(Schema.annotateKey({ description: "GraphQL node ID of the run." })),
10
+ repository_id: Long.pipe(
11
+ Schema.annotateKey({ description: "ID of the repository the run belongs to." }),
12
+ ),
13
+ workflow_id: Long.pipe(Schema.annotateKey({ description: "ID of the workflow." })),
14
+ name: Schema.NullOr(Schema.String).pipe(Schema.annotateKey({ description: "Workflow name." })),
15
+ display_title: Schema.String.pipe(
16
+ Schema.annotateKey({ description: "Title of the run, such as the commit message." }),
17
+ ),
18
+ path: Schema.String.pipe(Schema.annotateKey({ description: "Path of the workflow file." })),
19
+ event: Schema.String.pipe(
20
+ Schema.annotateKey({ description: "Event that started the run, such as push." }),
21
+ ),
22
+ status: Schema.NullOr(Schema.String).pipe(
23
+ Schema.annotateKey({ description: "Status, such as queued, in_progress, or completed." }),
24
+ ),
25
+ conclusion: Schema.NullOr(Schema.String).pipe(
26
+ Schema.annotateKey({ description: "Result of a completed run, such as success or failure." }),
27
+ ),
28
+ head_branch: Schema.NullOr(Schema.String).pipe(
29
+ Schema.annotateKey({ description: "Branch the run is for." }),
30
+ ),
31
+ head_sha: Schema.String.pipe(
32
+ Schema.annotateKey({ description: "SHA of the commit the run is for." }),
33
+ ),
34
+ run_number: Long.pipe(Schema.annotateKey({ description: "Run number of the workflow." })),
35
+ run_attempt: Schema.optionalKey(Long).pipe(
36
+ Schema.annotateKey({ description: "Attempt number. Re-runs count up." }),
37
+ ),
38
+ check_suite_id: Schema.optionalKey(Long).pipe(
39
+ Schema.annotateKey({ description: "ID of the run's check suite." }),
40
+ ),
41
+ actor: Schema.optionalKey(user).pipe(
42
+ Schema.annotateKey({ description: "Account that started the first attempt." }),
43
+ ),
44
+ triggering_actor: Schema.optionalKey(user).pipe(
45
+ Schema.annotateKey({ description: "Account that started the latest attempt." }),
46
+ ),
47
+ pull_requests: Schema.Array(
48
+ Schema.Struct({
49
+ id: Long.pipe(Schema.annotateKey({ description: "Pull request ID." })),
50
+ number: Long.pipe(Schema.annotateKey({ description: "Pull request number." })),
51
+ }),
52
+ ).pipe(Schema.annotateKey({ description: "Pull requests the run is for. Empty for forks." })),
53
+ created_at: Timestamp.pipe(Schema.annotateKey({ description: "Time the run was created." })),
54
+ run_started_at: Schema.optionalKey(Timestamp).pipe(
55
+ Schema.annotateKey({ description: "Time the latest attempt started." }),
56
+ ),
57
+ html_url: Schema.String.pipe(Schema.annotateKey({ description: "Run page on GitHub." })),
58
+ })
59
+ .annotate({
60
+ description: "GitHub Actions workflow runs in the installation's repositories.",
61
+ })
62
+ .pipe(Iceberg.identifier("id"));
63
+
64
+ export type WorkflowRun = Schema.Schema.Type<typeof WorkflowRunSchema>;
65
+
66
+ export const WorkflowRunObjectSchema = WorkflowRunSchema.mapFields((fields) => ({
67
+ ...Struct.omit(fields, ["version", "repository_id", "pull_requests"]),
68
+ updated_at: Timestamp,
69
+ pull_requests: Schema.NullOr(fields.pull_requests),
70
+ }));
71
+
72
+ 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.layerArrowFlightConfig(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 connector"));
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
+ };