@pepys/source-github 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p>
10
10
  <a href="../../docs/README.md">Documentation</a> •
11
- <a href="../../docs/design/v1-plug-ins.md">The plug-ins of v1</a>
11
+ <a href="../../docs/architecture/plug-ins/github.md">The GitHub plug-in</a>
12
12
  </p>
13
13
  </div>
14
14
 
@@ -20,7 +20,23 @@ with its consumer.
20
20
  ## Install
21
21
 
22
22
  ```sh
23
- bun add @pepys/source-github
23
+ bun add @pepys/source-github @pepys/kit
24
+ bun add -d @pepys/cli
25
+ ```
26
+
27
+ Add the plug-in to a deployment, then build its consumers. The build compiles each consumer, which
28
+ needs zig and wasm-tools, as the [CLI README](../cli/README.md) says.
29
+
30
+ ```ts
31
+ // deployment.ts
32
+ import { defineDeployment } from '@pepys/kit';
33
+ import { githubPlugin } from '@pepys/source-github';
34
+
35
+ export const deployment = defineDeployment({ name: 'home', plugins: [githubPlugin] });
36
+ ```
37
+
38
+ ```sh
39
+ bunx pepys build deployment.ts --out build
24
40
  ```
25
41
 
26
42
  ## Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pepys/source-github",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The pepys GitHub plug-in: pull requests, releases, commits and deploys",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,9 +19,9 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@pepys/api": "0.2.0",
23
- "@pepys/ingest": "0.2.0",
24
- "@pepys/kit": "0.2.0",
22
+ "@pepys/api": "0.3.0",
23
+ "@pepys/ingest": "0.3.0",
24
+ "@pepys/kit": "0.3.0",
25
25
  "@zgeoff/std": "0.4.3",
26
26
  "kysely": "0.29.8",
27
27
  "ts-pattern": "5.9.2",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@msw/data": "1.1.8",
32
- "@pepys/testing": "0.2.0",
32
+ "@orpc/client": "2.0.0-beta.50",
33
+ "@pepys/testing": "0.3.0",
33
34
  "msw": "3.0.3",
34
35
  "tiny-invariant": "1.3.3"
35
36
  }
@@ -2,7 +2,8 @@ import { serveAPI } from '@pepys/api';
2
2
  import { defineDeployment } from '@pepys/kit';
3
3
  import { githubPlugin } from '../github-plugin.ts';
4
4
 
5
- serveAPI({
5
+ await serveAPI({
6
6
  deployment: defineDeployment({ name: 'github', plugins: [githubPlugin] }),
7
7
  environment: process.env,
8
+ argv: process.argv.slice(2),
8
9
  });
@@ -85,6 +85,10 @@ export function updateGithubDeployModel(model: GithubDeployModel, message: Githu
85
85
  return message.kind === 'reset' ? initGithubDeployModel() : { ...model, input: message.input };
86
86
  }
87
87
 
88
+ declare const factTag: unique symbol;
89
+
90
+ export type FactID = string & { readonly [factTag]: 'FactID' };
91
+
88
92
  export interface GithubRefPushedPayload {
89
93
  ref: string;
90
94
  repository: GithubRefPushedPayloadRepository;
@@ -103,8 +107,8 @@ export interface GithubDeploymentPayloadDeployment {
103
107
  }
104
108
 
105
109
  export type GithubDeployFact =
106
- | { id: string; type: 'github.ref.pushed'; entity: string; at: string; payload: GithubRefPushedPayload }
107
- | { id: string; type: 'github.deployment.created' | 'github.deployment.status_created'; entity: string; at: string; payload: GithubDeploymentPayload };
110
+ | { id: FactID; type: 'github.ref.pushed'; entity: EntityID; at: string; payload: GithubRefPushedPayload }
111
+ | { id: FactID; type: 'github.deployment.created' | 'github.deployment.status_created'; entity: EntityID; at: string; payload: GithubDeploymentPayload };
108
112
 
109
113
  export function parseGithubDeployFact(factJSON: string): GithubDeployFact | undefined {
110
114
  const parsed: unknown = JSON.parse(factJSON);
@@ -127,6 +131,10 @@ function decodeGithubDeployFact(value: unknown): GithubDeployFact | undefined {
127
131
  return undefined;
128
132
  }
129
133
 
134
+ if (!isFactID(id) || !isFactEntityID(entity)) {
135
+ return undefined;
136
+ }
137
+
130
138
  if (type === 'github.ref.pushed') {
131
139
  const decoded = decodeGithubRefPushedPayload(payload);
132
140
 
@@ -150,6 +158,14 @@ function decodeGithubDeployFact(value: unknown): GithubDeployFact | undefined {
150
158
  return undefined;
151
159
  }
152
160
 
161
+ function isFactID(text: string): text is FactID {
162
+ return text.length > 0;
163
+ }
164
+
165
+ function isFactEntityID(text: string): text is EntityID {
166
+ return text.length > 0;
167
+ }
168
+
153
169
  function decodeGithubRefPushedPayload(value: unknown): GithubRefPushedPayload | undefined {
154
170
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
155
171
  return undefined;
@@ -45,6 +45,10 @@ export function updateGithubPullRequestModel(model: GithubPullRequestModel, mess
45
45
  return message.kind === 'reset' ? initGithubPullRequestModel() : { ...model, input: message.input };
46
46
  }
47
47
 
48
+ declare const factTag: unique symbol;
49
+
50
+ export type FactID = string & { readonly [factTag]: 'FactID' };
51
+
48
52
  export interface GithubPullRequestPayload {
49
53
  action: string;
50
54
  pull_request: GithubPullRequestPayloadPullRequest;
@@ -56,7 +60,7 @@ export interface GithubPullRequestPayloadPullRequest {
56
60
  }
57
61
 
58
62
  export type GithubPullRequestFact =
59
- | { id: string; type: 'github.pull_request.assigned' | 'github.pull_request.auto_merge_disabled' | 'github.pull_request.auto_merge_enabled' | 'github.pull_request.closed' | 'github.pull_request.converted_to_draft' | 'github.pull_request.demilestoned' | 'github.pull_request.dequeued' | 'github.pull_request.edited' | 'github.pull_request.enqueued' | 'github.pull_request.labeled' | 'github.pull_request.locked' | 'github.pull_request.milestoned' | 'github.pull_request.opened' | 'github.pull_request.ready_for_review' | 'github.pull_request.reopened' | 'github.pull_request.resynced' | 'github.pull_request.review_request_removed' | 'github.pull_request.review_requested' | 'github.pull_request.synchronize' | 'github.pull_request.unassigned' | 'github.pull_request.unlabeled' | 'github.pull_request.unlocked'; entity: string; at: string; payload: GithubPullRequestPayload };
63
+ | { id: FactID; type: 'github.pull_request.assigned' | 'github.pull_request.auto_merge_disabled' | 'github.pull_request.auto_merge_enabled' | 'github.pull_request.closed' | 'github.pull_request.converted_to_draft' | 'github.pull_request.demilestoned' | 'github.pull_request.dequeued' | 'github.pull_request.edited' | 'github.pull_request.enqueued' | 'github.pull_request.labeled' | 'github.pull_request.locked' | 'github.pull_request.milestoned' | 'github.pull_request.opened' | 'github.pull_request.ready_for_review' | 'github.pull_request.reopened' | 'github.pull_request.resynced' | 'github.pull_request.review_request_removed' | 'github.pull_request.review_requested' | 'github.pull_request.synchronize' | 'github.pull_request.unassigned' | 'github.pull_request.unlabeled' | 'github.pull_request.unlocked'; entity: EntityID; at: string; payload: GithubPullRequestPayload };
60
64
 
61
65
  export function parseGithubPullRequestFact(factJSON: string): GithubPullRequestFact | undefined {
62
66
  const parsed: unknown = JSON.parse(factJSON);
@@ -79,6 +83,10 @@ function decodeGithubPullRequestFact(value: unknown): GithubPullRequestFact | un
79
83
  return undefined;
80
84
  }
81
85
 
86
+ if (!isFactID(id) || !isFactEntityID(entity)) {
87
+ return undefined;
88
+ }
89
+
82
90
  if (type === 'github.pull_request.assigned' || type === 'github.pull_request.auto_merge_disabled' || type === 'github.pull_request.auto_merge_enabled' || type === 'github.pull_request.closed' || type === 'github.pull_request.converted_to_draft' || type === 'github.pull_request.demilestoned' || type === 'github.pull_request.dequeued' || type === 'github.pull_request.edited' || type === 'github.pull_request.enqueued' || type === 'github.pull_request.labeled' || type === 'github.pull_request.locked' || type === 'github.pull_request.milestoned' || type === 'github.pull_request.opened' || type === 'github.pull_request.ready_for_review' || type === 'github.pull_request.reopened' || type === 'github.pull_request.resynced' || type === 'github.pull_request.review_request_removed' || type === 'github.pull_request.review_requested' || type === 'github.pull_request.synchronize' || type === 'github.pull_request.unassigned' || type === 'github.pull_request.unlabeled' || type === 'github.pull_request.unlocked') {
83
91
  const decoded = decodeGithubPullRequestPayload(payload);
84
92
 
@@ -92,6 +100,14 @@ function decodeGithubPullRequestFact(value: unknown): GithubPullRequestFact | un
92
100
  return undefined;
93
101
  }
94
102
 
103
+ function isFactID(text: string): text is FactID {
104
+ return text.length > 0;
105
+ }
106
+
107
+ function isFactEntityID(text: string): text is EntityID {
108
+ return text.length > 0;
109
+ }
110
+
95
111
  function decodeGithubPullRequestPayload(value: unknown): GithubPullRequestPayload | undefined {
96
112
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
97
113
  return undefined;
@@ -45,6 +45,10 @@ export function updateGithubRefModel(model: GithubRefModel, message: GithubRefMe
45
45
  return message.kind === 'reset' ? initGithubRefModel() : { ...model, input: message.input };
46
46
  }
47
47
 
48
+ declare const factTag: unique symbol;
49
+
50
+ export type FactID = string & { readonly [factTag]: 'FactID' };
51
+
48
52
  export interface GithubRefPushedPayload {
49
53
  ref: string;
50
54
  repository: GithubRefPushedPayloadRepository;
@@ -55,7 +59,7 @@ export interface GithubRefPushedPayloadRepository {
55
59
  }
56
60
 
57
61
  export type GithubRefFact =
58
- | { id: string; type: 'github.ref.pushed'; entity: string; at: string; payload: GithubRefPushedPayload };
62
+ | { id: FactID; type: 'github.ref.pushed'; entity: EntityID; at: string; payload: GithubRefPushedPayload };
59
63
 
60
64
  export function parseGithubRefFact(factJSON: string): GithubRefFact | undefined {
61
65
  const parsed: unknown = JSON.parse(factJSON);
@@ -78,6 +82,10 @@ function decodeGithubRefFact(value: unknown): GithubRefFact | undefined {
78
82
  return undefined;
79
83
  }
80
84
 
85
+ if (!isFactID(id) || !isFactEntityID(entity)) {
86
+ return undefined;
87
+ }
88
+
81
89
  if (type === 'github.ref.pushed') {
82
90
  const decoded = decodeGithubRefPushedPayload(payload);
83
91
 
@@ -91,6 +99,14 @@ function decodeGithubRefFact(value: unknown): GithubRefFact | undefined {
91
99
  return undefined;
92
100
  }
93
101
 
102
+ function isFactID(text: string): text is FactID {
103
+ return text.length > 0;
104
+ }
105
+
106
+ function isFactEntityID(text: string): text is EntityID {
107
+ return text.length > 0;
108
+ }
109
+
94
110
  function decodeGithubRefPushedPayload(value: unknown): GithubRefPushedPayload | undefined {
95
111
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
96
112
  return undefined;
@@ -45,6 +45,10 @@ export function updateGithubReleaseModel(model: GithubReleaseModel, message: Git
45
45
  return message.kind === 'reset' ? initGithubReleaseModel() : { ...model, input: message.input };
46
46
  }
47
47
 
48
+ declare const factTag: unique symbol;
49
+
50
+ export type FactID = string & { readonly [factTag]: 'FactID' };
51
+
48
52
  export interface GithubReleasePayload {
49
53
  action: string;
50
54
  release: GithubReleasePayloadRelease;
@@ -55,7 +59,7 @@ export interface GithubReleasePayloadRelease {
55
59
  }
56
60
 
57
61
  export type GithubReleaseFact =
58
- | { id: string; type: 'github.release.created' | 'github.release.deleted' | 'github.release.edited' | 'github.release.prereleased' | 'github.release.published' | 'github.release.released' | 'github.release.resynced' | 'github.release.unpublished'; entity: string; at: string; payload: GithubReleasePayload };
62
+ | { id: FactID; type: 'github.release.created' | 'github.release.deleted' | 'github.release.edited' | 'github.release.prereleased' | 'github.release.published' | 'github.release.released' | 'github.release.resynced' | 'github.release.unpublished'; entity: EntityID; at: string; payload: GithubReleasePayload };
59
63
 
60
64
  export function parseGithubReleaseFact(factJSON: string): GithubReleaseFact | undefined {
61
65
  const parsed: unknown = JSON.parse(factJSON);
@@ -78,6 +82,10 @@ function decodeGithubReleaseFact(value: unknown): GithubReleaseFact | undefined
78
82
  return undefined;
79
83
  }
80
84
 
85
+ if (!isFactID(id) || !isFactEntityID(entity)) {
86
+ return undefined;
87
+ }
88
+
81
89
  if (type === 'github.release.created' || type === 'github.release.deleted' || type === 'github.release.edited' || type === 'github.release.prereleased' || type === 'github.release.published' || type === 'github.release.released' || type === 'github.release.resynced' || type === 'github.release.unpublished') {
82
90
  const decoded = decodeGithubReleasePayload(payload);
83
91
 
@@ -91,6 +99,14 @@ function decodeGithubReleaseFact(value: unknown): GithubReleaseFact | undefined
91
99
  return undefined;
92
100
  }
93
101
 
102
+ function isFactID(text: string): text is FactID {
103
+ return text.length > 0;
104
+ }
105
+
106
+ function isFactEntityID(text: string): text is EntityID {
107
+ return text.length > 0;
108
+ }
109
+
94
110
  function decodeGithubReleasePayload(value: unknown): GithubReleasePayload | undefined {
95
111
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
96
112
  return undefined;
@@ -29,4 +29,5 @@ SELECT
29
29
  "url" AS "url",
30
30
  NULL AS "actor",
31
31
  "updated_at" AS "updated_at"
32
- FROM "github_release";
32
+ FROM "github_release"
33
+ WHERE "deleted_at" IS NULL;
@@ -1,7 +1,8 @@
1
1
  import { defineConsumer, defineTable } from '@pepys/kit';
2
2
  import { releaseFamily } from './families/release-family.ts';
3
3
 
4
- // One row per release, never deleted: a release GitHub deleted keeps its row with `deleted_at`.
4
+ // One row per release, never deleted: a release GitHub deleted keeps its row with `deleted_at`,
5
+ // which leaves it out of the `entity` view.
5
6
  export const githubReleaseConsumer = defineConsumer({
6
7
  name: 'github_release',
7
8
  entry: new URL('consumers/github-release.ts', import.meta.url),
@@ -36,6 +37,7 @@ export const githubReleaseConsumer = defineConsumer({
36
37
  url: 'url',
37
38
  actor: null,
38
39
  updatedAt: 'updated_at',
40
+ deletedAt: 'deleted_at',
39
41
  },
40
42
  ],
41
43
  reads: ['github_release'],
@@ -19,7 +19,7 @@ export async function pullFailedDeliveries(
19
19
  cursor: string | null,
20
20
  context: PullContext,
21
21
  ): Promise<Pulled> {
22
- const appJWT = findAppJWT(context.env, Date.now());
22
+ const appJWT = findAppJWT(context.env, context.now());
23
23
 
24
24
  if (appJWT === undefined) {
25
25
  return { kind: 'rejected', reason: 'the GitHub App private key cannot sign a token' };
@@ -1,10 +1,10 @@
1
- import type { ResyncMark } from '@pepys/kit';
1
+ import type { EntityID, ResyncMark } from '@pepys/kit';
2
2
 
3
3
  // One marked object that the resync pull reads again: its repository, `owner/name`, its API path,
4
4
  // and the digest of the last body ingest stored for it.
5
5
  export interface ResyncTarget {
6
6
  kind: 'pull_request' | 'release';
7
- entity: string;
7
+ entity: EntityID;
8
8
  repository: string;
9
9
  path: string;
10
10
  lastBodyDigest: string | undefined;
@@ -23,7 +23,7 @@ export async function pullMarkedObjects(
23
23
  return { kind: 'batch', batch: { facts: [] }, cursor: 'resynced', isDone: true };
24
24
  }
25
25
 
26
- const appJWT = findAppJWT(context.env, Date.now());
26
+ const appJWT = findAppJWT(context.env, context.now());
27
27
 
28
28
  if (appJWT === undefined) {
29
29
  return { kind: 'rejected', reason: 'the GitHub App private key cannot sign a token' };
@@ -1,4 +1,4 @@
1
- import { defineQuery } from '@pepys/kit';
1
+ import { defineQuery, entityIDSchema, isEntityID } from '@pepys/kit';
2
2
  import { z } from 'zod';
3
3
  import { deploySignals } from '../utils/deploy-signals.ts';
4
4
  import { githubDeliveryConsumers } from './github-delivery-consumers.ts';
@@ -9,12 +9,12 @@ import {
9
9
 
10
10
  const pullRequestsSchema = z.array(z.string()).max(200);
11
11
  const nameSchema = z.string().min(1);
12
- const releaseSchema = z.object({ id: z.string(), tag: z.string(), at: z.string() });
12
+ const releaseSchema = z.object({ id: entityIDSchema, tag: z.string(), at: z.string() });
13
13
  const signalSchema = z.enum(deploySignals);
14
- const deploySchema = z.object({ id: z.string(), at: z.string(), signal: signalSchema });
14
+ const deploySchema = z.object({ id: entityIDSchema, at: z.string(), signal: signalSchema });
15
15
 
16
16
  const deliverySchema = z.object({
17
- pullRequest: z.string(),
17
+ pullRequest: entityIDSchema,
18
18
  mergedAt: z.string().nullable(),
19
19
  release: releaseSchema.nullable(),
20
20
  deploy: deploySchema.nullable(),
@@ -32,7 +32,8 @@ export const pullRequestDeliveryQuery = defineQuery({
32
32
  output: z.array(deliverySchema),
33
33
  reads: githubDeliveryConsumers,
34
34
  handler: async (call) => {
35
- const [first, ...rest] = call.input.pullRequests;
35
+ // text that is no entity ID names no stored pull request, so it has no row either way
36
+ const [first, ...rest] = call.input.pullRequests.filter((id) => isEntityID(id));
36
37
 
37
38
  if (first === undefined) {
38
39
  return [];
@@ -1,3 +1,5 @@
1
+ import { entityIDSchema } from '@pepys/kit';
2
+ import type { EntityID } from '@pepys/kit';
1
3
  import { sql } from 'kysely';
2
4
  import type { RawBuilder } from 'kysely';
3
5
  import { z } from 'zod';
@@ -6,7 +8,7 @@ import type { DeploySignal } from '../utils/deploy-signals.ts';
6
8
 
7
9
  interface DeliveryOptions {
8
10
  // at least one
9
- pullRequests: readonly [string, ...string[]];
11
+ pullRequests: readonly [EntityID, ...EntityID[]];
10
12
  environment: string;
11
13
 
12
14
  // Absent: a deploy of any target, and of none, counts. Given: only a deploy of that target.
@@ -14,8 +16,8 @@ interface DeliveryOptions {
14
16
  }
15
17
 
16
18
  // The statement that ties pull requests to releases and deploys. The rules it holds are in the
17
- // "Delivery query" section of the plug-ins design doc. Run it with `.execute(db)`, and turn its rows
18
- // into deliveries with the parse function below.
19
+ // "The delivery query" section of docs/architecture/plug-ins/github.md. Run it with `.execute(db)`,
20
+ // and turn its rows into deliveries with the parse function below.
19
21
  export function selectPullRequestDelivery(options: DeliveryOptions): RawBuilder<unknown> {
20
22
  return sql`select
21
23
  pull_request.id as "pullRequest",
@@ -115,22 +117,22 @@ function selectFirstDeployID(options: DeliveryOptions): RawBuilder<unknown> {
115
117
  }
116
118
 
117
119
  export interface PullRequestDelivery {
118
- pullRequest: string;
120
+ pullRequest: EntityID;
119
121
  mergedAt: string | null;
120
- release: { id: string; tag: string; at: string } | null;
121
- deploy: { id: string; at: string; signal: DeploySignal } | null;
122
+ release: { id: EntityID; tag: string; at: string } | null;
123
+ deploy: { id: EntityID; at: string; signal: DeploySignal } | null;
122
124
  }
123
125
 
124
126
  const textSchema = z.string().nullable();
125
127
 
126
128
  // One row per pull request, as the statement selects it.
127
129
  const deliveryRowSchema = z.object({
128
- pullRequest: z.string(),
130
+ pullRequest: entityIDSchema,
129
131
  mergedAt: textSchema,
130
- releaseID: textSchema,
132
+ releaseID: entityIDSchema.nullable(),
131
133
  releaseTag: textSchema,
132
134
  releaseAt: textSchema,
133
- deployID: textSchema,
135
+ deployID: entityIDSchema.nullable(),
134
136
  deployAt: textSchema,
135
137
  deploySignal: z.enum(deploySignals).nullable(),
136
138
  });