@alchemy.run/pkg 2.0.0-beta.79 → 2.0.0-beta.81
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 +17 -5
- package/dist/Manifest.d.ts.map +1 -1
- package/dist/Manifest.js.map +1 -1
- package/dist/Protocol.d.ts +3 -3
- package/dist/Protocol.d.ts.map +1 -1
- package/dist/Protocol.js +6 -15
- package/dist/Protocol.js.map +1 -1
- package/dist/Registry/Bindings.d.ts.map +1 -1
- package/dist/Registry/Bindings.js.map +1 -1
- package/dist/Registry/GitHub.d.ts +7 -7
- package/dist/Registry/GitHub.d.ts.map +1 -1
- package/dist/Registry/GitHub.js +20 -45
- package/dist/Registry/GitHub.js.map +1 -1
- package/dist/Registry/Handler.d.ts +5 -5
- package/dist/Registry/Handler.d.ts.map +1 -1
- package/dist/Registry/Handler.js +26 -27
- package/dist/Registry/Handler.js.map +1 -1
- package/dist/Registry/KVCache.d.ts.map +1 -1
- package/dist/Registry/KVCache.js +1 -1
- package/dist/Registry/KVCache.js.map +1 -1
- package/dist/Registry/Sweep.d.ts +1 -1
- package/dist/Registry/Sweep.d.ts.map +1 -1
- package/dist/Registry/Sweep.js +3 -8
- package/dist/Registry/Sweep.js.map +1 -1
- package/dist/Registry/Tags.d.ts +8 -8
- package/dist/Registry/Tags.d.ts.map +1 -1
- package/dist/Registry/Tags.js +1 -1
- package/dist/Registry/Tags.js.map +1 -1
- package/dist/Registry.d.ts +2 -2
- package/dist/Registry.d.ts.map +1 -1
- package/dist/Registry.js +1 -1
- package/dist/Registry.js.map +1 -1
- package/dist/cli/bin.d.ts +8 -4
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +22 -10
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/pack.d.ts +11 -4
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +91 -30
- package/dist/cli/pack.js.map +1 -1
- package/dist/cli/publish.d.ts +1 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/publish.js +6 -2
- package/dist/cli/publish.js.map +1 -1
- package/package.json +23 -23
- package/src/Manifest.ts +1 -3
- package/src/Protocol.ts +15 -31
- package/src/Registry/Bindings.ts +1 -4
- package/src/Registry/GitHub.ts +34 -86
- package/src/Registry/Handler.ts +46 -90
- package/src/Registry/KVCache.ts +6 -17
- package/src/Registry/Sweep.ts +13 -26
- package/src/Registry/Tags.ts +8 -16
- package/src/Registry.ts +5 -15
- package/src/cli/bin.ts +35 -20
- package/src/cli/pack.ts +142 -82
- package/src/cli/publish.ts +16 -23
package/src/Registry/GitHub.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
1
2
|
import * as Github from "@distilled.cloud/github";
|
|
2
3
|
import * as Actions from "@distilled.cloud/github/actions";
|
|
3
4
|
import * as Apps from "@distilled.cloud/github/apps";
|
|
4
5
|
import * as Checks from "@distilled.cloud/github/checks";
|
|
5
6
|
import * as Issues from "@distilled.cloud/github/issues";
|
|
6
7
|
import * as Pulls from "@distilled.cloud/github/pulls";
|
|
7
|
-
import * as Repos from "@distilled.cloud/github/repos";
|
|
8
8
|
import * as Arr from "effect/Array";
|
|
9
9
|
import * as Clock from "effect/Clock";
|
|
10
10
|
import * as Context from "effect/Context";
|
|
@@ -12,20 +12,17 @@ import * as Data from "effect/Data";
|
|
|
12
12
|
import * as DateTime from "effect/DateTime";
|
|
13
13
|
import * as Duration from "effect/Duration";
|
|
14
14
|
import * as Effect from "effect/Effect";
|
|
15
|
-
import
|
|
15
|
+
import { Base64Url } from "effect/encoding";
|
|
16
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
16
17
|
import * as Layer from "effect/Layer";
|
|
17
18
|
import * as Option from "effect/Option";
|
|
18
19
|
import * as Redacted from "effect/Redacted";
|
|
19
20
|
import * as Schema from "effect/Schema";
|
|
20
21
|
import * as Stream from "effect/Stream";
|
|
21
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
22
|
-
import * as crypto from "node:crypto";
|
|
23
22
|
import { COMMENT_MARKER, RegistryConfig } from "./Bindings.ts";
|
|
24
23
|
import * as KVCache from "./KVCache.ts";
|
|
25
24
|
|
|
26
|
-
export class CryptoError extends Data.TaggedError("CryptoError")<{
|
|
27
|
-
readonly message: string;
|
|
28
|
-
}> {}
|
|
25
|
+
export class CryptoError extends Data.TaggedError("CryptoError")<{ readonly message: string }> {}
|
|
29
26
|
|
|
30
27
|
export class AppNotInstalled extends Data.TaggedError("AppNotInstalled")<{
|
|
31
28
|
readonly repo: string;
|
|
@@ -39,24 +36,18 @@ export class AppNotInstalled extends Data.TaggedError("AppNotInstalled")<{
|
|
|
39
36
|
export const importPrivateKey = (pem: string) =>
|
|
40
37
|
Effect.try({
|
|
41
38
|
try: () => crypto.createPrivateKey({ key: pem }),
|
|
42
|
-
catch: (cause) =>
|
|
43
|
-
new CryptoError({ message: `invalid private key: ${cause}` }),
|
|
39
|
+
catch: (cause) => new CryptoError({ message: `invalid private key: ${cause}` }),
|
|
44
40
|
});
|
|
45
41
|
|
|
46
42
|
/** Sign a compact RS256 JWT, used to authenticate as the GitHub App. */
|
|
47
|
-
export const signJwt = (
|
|
48
|
-
claims: Record<string, unknown>,
|
|
49
|
-
key: crypto.KeyObject,
|
|
50
|
-
) =>
|
|
43
|
+
export const signJwt = (claims: Record<string, unknown>, key: crypto.KeyObject) =>
|
|
51
44
|
Effect.try({
|
|
52
45
|
try: () => {
|
|
53
|
-
const header =
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
const payload = Encoding.encodeBase64Url(JSON.stringify(claims));
|
|
46
|
+
const header = Base64Url.encode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
47
|
+
const payload = Base64Url.encode(JSON.stringify(claims));
|
|
57
48
|
const input = `${header}.${payload}`;
|
|
58
49
|
const signature = crypto.sign("sha256", Buffer.from(input), key);
|
|
59
|
-
return `${input}.${
|
|
50
|
+
return `${input}.${Base64Url.encode(new Uint8Array(signature))}`;
|
|
60
51
|
},
|
|
61
52
|
catch: (cause) => new CryptoError({ message: `signing failed: ${cause}` }),
|
|
62
53
|
});
|
|
@@ -70,21 +61,14 @@ export const renderInstalls = (
|
|
|
70
61
|
packages: ReadonlyArray<{ name: string; group: string; url: string }>,
|
|
71
62
|
groups: ReadonlyArray<{ name: string; collapsed: boolean }>,
|
|
72
63
|
) => {
|
|
73
|
-
const collapsed = new Set(
|
|
74
|
-
groups.filter((g) => g.collapsed).map((g) => g.name),
|
|
75
|
-
);
|
|
64
|
+
const collapsed = new Set(groups.filter((g) => g.collapsed).map((g) => g.name));
|
|
76
65
|
// One code block per package so each command has its own copy button, in
|
|
77
66
|
// the order the manifest lists them, which is the order they were given
|
|
78
67
|
// to `pkg pack`. Blank lines around the markdown inside `<details>` are
|
|
79
68
|
// what make GitHub render it.
|
|
80
69
|
return Object.entries(Arr.groupBy(packages, (pkg) => pkg.group))
|
|
81
70
|
.flatMap(([group, members]) => {
|
|
82
|
-
const installs = members.flatMap(({ url }) => [
|
|
83
|
-
"```sh",
|
|
84
|
-
`pnpm install ${url}`,
|
|
85
|
-
"```",
|
|
86
|
-
"",
|
|
87
|
-
]);
|
|
71
|
+
const installs = members.flatMap(({ url }) => ["```sh", `pnpm install ${url}`, "```", ""]);
|
|
88
72
|
return collapsed.has(group)
|
|
89
73
|
? [
|
|
90
74
|
"<details>",
|
|
@@ -178,10 +162,7 @@ const make = Effect.gen(function* () {
|
|
|
178
162
|
const pem = yield* config.github.privateKey;
|
|
179
163
|
const key = yield* importPrivateKey(Redacted.value(pem));
|
|
180
164
|
const now = Math.floor((yield* Clock.currentTimeMillis) / 1000);
|
|
181
|
-
return yield* signJwt(
|
|
182
|
-
{ iat: now - 60, exp: now + 540, iss: yield* config.github.appId },
|
|
183
|
-
key,
|
|
184
|
-
);
|
|
165
|
+
return yield* signJwt({ iat: now - 60, exp: now + 540, iss: yield* config.github.appId }, key);
|
|
185
166
|
});
|
|
186
167
|
|
|
187
168
|
// Failures are not kept: the next call retries the mint.
|
|
@@ -191,17 +172,12 @@ const make = Effect.gen(function* () {
|
|
|
191
172
|
(repo) =>
|
|
192
173
|
Effect.gen(function* () {
|
|
193
174
|
const jwt = yield* appJwt;
|
|
194
|
-
const installation = yield* as(
|
|
195
|
-
jwt,
|
|
196
|
-
Apps.getRepoInstallation(split(repo)),
|
|
197
|
-
).pipe(
|
|
175
|
+
const installation = yield* as(jwt, Apps.getRepoInstallation(split(repo))).pipe(
|
|
198
176
|
Effect.catchTag("NotFound", () => new AppNotInstalled({ repo })),
|
|
199
177
|
);
|
|
200
178
|
const access = yield* as(
|
|
201
179
|
jwt,
|
|
202
|
-
Apps.createInstallationAccessToken({
|
|
203
|
-
installation_id: installation.id,
|
|
204
|
-
}),
|
|
180
|
+
Apps.createInstallationAccessToken({ installation_id: installation.id }),
|
|
205
181
|
);
|
|
206
182
|
const now = yield* Clock.currentTimeMillis;
|
|
207
183
|
return {
|
|
@@ -215,17 +191,12 @@ const make = Effect.gen(function* () {
|
|
|
215
191
|
);
|
|
216
192
|
|
|
217
193
|
/** A GitHub operation run as the App's installation on `repo`. */
|
|
218
|
-
const asInstallation = <A, E, R>(
|
|
219
|
-
repo
|
|
220
|
-
operation: Effect.Effect<A, E, R>,
|
|
221
|
-
) => Effect.flatMap(tokens(repo), (token) => as(token, operation));
|
|
194
|
+
const asInstallation = <A, E, R>(repo: string, operation: Effect.Effect<A, E, R>) =>
|
|
195
|
+
Effect.flatMap(tokens(repo), (token) => as(token, operation));
|
|
222
196
|
|
|
223
197
|
return {
|
|
224
198
|
getRun: (repo: string, runId: number) =>
|
|
225
|
-
asInstallation(
|
|
226
|
-
repo,
|
|
227
|
-
Actions.getWorkflowRun({ ...split(repo), run_id: runId }),
|
|
228
|
-
),
|
|
199
|
+
asInstallation(repo, Actions.getWorkflowRun({ ...split(repo), run_id: runId })),
|
|
229
200
|
|
|
230
201
|
/**
|
|
231
202
|
* Artifacts named `name` uploaded to a run so far, including by jobs
|
|
@@ -234,24 +205,21 @@ const make = Effect.gen(function* () {
|
|
|
234
205
|
listRunArtifacts: (repo: string, runId: number, name: string) =>
|
|
235
206
|
asInstallation(
|
|
236
207
|
repo,
|
|
237
|
-
Actions.listWorkflowRunArtifacts({
|
|
238
|
-
...split(repo),
|
|
239
|
-
run_id: runId,
|
|
240
|
-
name,
|
|
241
|
-
per_page: 100,
|
|
242
|
-
}),
|
|
208
|
+
Actions.listWorkflowRunArtifacts({ ...split(repo), run_id: runId, name, per_page: 100 }),
|
|
243
209
|
).pipe(Effect.map((page) => page.artifacts)),
|
|
244
210
|
|
|
245
211
|
/**
|
|
246
|
-
*
|
|
247
|
-
* repository the commit was pushed to.
|
|
212
|
+
* Open pull requests against `repo` whose head is `sha` in `headRepo`, the
|
|
213
|
+
* repository the commit was pushed to. Commit-associated PR lookups can
|
|
214
|
+
* return no results for forks, so discover candidates by owner and branch.
|
|
248
215
|
*/
|
|
249
|
-
pullRequestsForCommit: (repo: string, headRepo: string, sha: string) =>
|
|
216
|
+
pullRequestsForCommit: (repo: string, headRepo: string, headBranch: string, sha: string) =>
|
|
250
217
|
asInstallation(
|
|
251
218
|
repo,
|
|
252
|
-
|
|
219
|
+
Pulls.list({
|
|
253
220
|
...split(repo),
|
|
254
|
-
|
|
221
|
+
head: `${split(headRepo).owner}:${headBranch}`,
|
|
222
|
+
state: "open",
|
|
255
223
|
per_page: 100,
|
|
256
224
|
}),
|
|
257
225
|
).pipe(
|
|
@@ -259,6 +227,7 @@ const make = Effect.gen(function* () {
|
|
|
259
227
|
pulls.filter(
|
|
260
228
|
(pr) =>
|
|
261
229
|
pr.head.sha === sha &&
|
|
230
|
+
pr.head.ref === headBranch &&
|
|
262
231
|
pr.head.repo?.full_name === headRepo &&
|
|
263
232
|
pr.base.repo.full_name === repo,
|
|
264
233
|
),
|
|
@@ -302,28 +271,16 @@ const make = Effect.gen(function* () {
|
|
|
302
271
|
* Create or update the comment on `issue` whose body starts with
|
|
303
272
|
* `marker`, looking through the first five pages of comments.
|
|
304
273
|
*/
|
|
305
|
-
upsertComment: (
|
|
306
|
-
repo: string,
|
|
307
|
-
issue: number,
|
|
308
|
-
marker: string,
|
|
309
|
-
body: string,
|
|
310
|
-
) =>
|
|
274
|
+
upsertComment: (repo: string, issue: number, marker: string, body: string) =>
|
|
311
275
|
Effect.gen(function* () {
|
|
312
276
|
const comments = Stream.paginate(1, (page) =>
|
|
313
277
|
asInstallation(
|
|
314
278
|
repo,
|
|
315
|
-
Issues.listComments({
|
|
316
|
-
...split(repo),
|
|
317
|
-
issue_number: issue,
|
|
318
|
-
per_page: 100,
|
|
319
|
-
page,
|
|
320
|
-
}),
|
|
279
|
+
Issues.listComments({ ...split(repo), issue_number: issue, per_page: 100, page }),
|
|
321
280
|
).pipe(
|
|
322
281
|
Effect.map((comments) => [
|
|
323
282
|
comments,
|
|
324
|
-
comments.length < 100 || page >= 5
|
|
325
|
-
? Option.none()
|
|
326
|
-
: Option.some(page + 1),
|
|
283
|
+
comments.length < 100 || page >= 5 ? Option.none() : Option.some(page + 1),
|
|
327
284
|
]),
|
|
328
285
|
),
|
|
329
286
|
);
|
|
@@ -337,29 +294,20 @@ const make = Effect.gen(function* () {
|
|
|
337
294
|
onNone: () =>
|
|
338
295
|
asInstallation(
|
|
339
296
|
repo,
|
|
340
|
-
Issues.createComment({
|
|
341
|
-
...split(repo),
|
|
342
|
-
issue_number: issue,
|
|
343
|
-
body,
|
|
344
|
-
}),
|
|
297
|
+
Issues.createComment({ ...split(repo), issue_number: issue, body }),
|
|
345
298
|
),
|
|
346
299
|
onSome: (comment) =>
|
|
347
300
|
asInstallation(
|
|
348
301
|
repo,
|
|
349
|
-
Issues.updateComment({
|
|
350
|
-
...split(repo),
|
|
351
|
-
comment_id: comment.id,
|
|
352
|
-
body,
|
|
353
|
-
}),
|
|
302
|
+
Issues.updateComment({ ...split(repo), comment_id: comment.id, body }),
|
|
354
303
|
),
|
|
355
304
|
});
|
|
356
305
|
}),
|
|
357
306
|
};
|
|
358
307
|
});
|
|
359
308
|
|
|
360
|
-
export class GitHubApp extends Context.Service<
|
|
361
|
-
GitHubApp,
|
|
362
|
-
|
|
363
|
-
>()("@alchemy.run/pkg/GitHubApp") {}
|
|
309
|
+
export class GitHubApp extends Context.Service<GitHubApp, Effect.Success<typeof make>>()(
|
|
310
|
+
"@alchemy.run/pkg/GitHubApp",
|
|
311
|
+
) {}
|
|
364
312
|
|
|
365
313
|
export const GitHubAppLive = Layer.effect(GitHubApp, make);
|
package/src/Registry/Handler.ts
CHANGED
|
@@ -4,18 +4,18 @@ import { sha256 } from "alchemy/Util/sha256";
|
|
|
4
4
|
import * as Clock from "effect/Clock";
|
|
5
5
|
import * as Duration from "effect/Duration";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
|
-
import
|
|
7
|
+
import { Hex } from "effect/encoding";
|
|
8
|
+
import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
|
|
9
|
+
import * as Etag from "effect/http/Etag";
|
|
10
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
11
|
+
import * as HttpPlatform from "effect/http/HttpPlatform";
|
|
12
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
13
|
+
import { HttpServerRequest } from "effect/http/HttpServerRequest";
|
|
14
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
8
15
|
import * as Layer from "effect/Layer";
|
|
9
16
|
import * as Path from "effect/Path";
|
|
10
17
|
import * as Result from "effect/Result";
|
|
11
18
|
import * as Schema from "effect/Schema";
|
|
12
|
-
import * as Etag from "effect/unstable/http/Etag";
|
|
13
|
-
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
14
|
-
import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
|
|
15
|
-
import * as HttpRouter from "effect/unstable/http/HttpRouter";
|
|
16
|
-
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
|
|
17
|
-
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
18
|
-
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
|
|
19
19
|
import { ManifestJson, type ManifestPackage } from "../Manifest.ts";
|
|
20
20
|
import {
|
|
21
21
|
BadRequest,
|
|
@@ -28,14 +28,7 @@ import {
|
|
|
28
28
|
RunRef,
|
|
29
29
|
Upstream,
|
|
30
30
|
} from "../Protocol.ts";
|
|
31
|
-
import {
|
|
32
|
-
Bucket,
|
|
33
|
-
Cache,
|
|
34
|
-
COMMENT_MARKER,
|
|
35
|
-
Database,
|
|
36
|
-
RegistryConfig,
|
|
37
|
-
tarballKey,
|
|
38
|
-
} from "./Bindings.ts";
|
|
31
|
+
import { Bucket, Cache, COMMENT_MARKER, Database, RegistryConfig, tarballKey } from "./Bindings.ts";
|
|
39
32
|
import * as GitHub from "./GitHub.ts";
|
|
40
33
|
import * as KVCache from "./KVCache.ts";
|
|
41
34
|
import { sweep } from "./Sweep.ts";
|
|
@@ -81,24 +74,24 @@ const isFork = (run: Run) => run.headRepo !== run.repo;
|
|
|
81
74
|
/**
|
|
82
75
|
* Tags every package in a publication receives, all derived from the run.
|
|
83
76
|
* Runs on the repository's own commits get the head commit, the short
|
|
84
|
-
* commit, `branch:<name>`, and `pr:N` for pull requests.
|
|
85
|
-
* only `pr:N
|
|
77
|
+
* commit, `branch:<name>`, and `pr:N` plus `pr:N:<short-sha>` for pull requests.
|
|
78
|
+
* A fork's run gets only `pr:N:<short-sha>`: commit tags are shared by every publisher, and a fork can
|
|
86
79
|
* run any commit it likes, including one already published from the
|
|
87
80
|
* repository, so letting it write them would let it repoint them.
|
|
88
81
|
*/
|
|
89
82
|
export const tagsFor = (run: Run): string[] => {
|
|
90
83
|
if (isFork(run)) {
|
|
91
|
-
return run.pr === null ? [] : [
|
|
84
|
+
return run.pr === null ? [] : [installTag(run)];
|
|
92
85
|
}
|
|
93
86
|
const tags = [run.headSha, run.headSha.slice(0, SHORT)];
|
|
94
|
-
if (run.pr !== null) tags.push(`pr:${run.pr}
|
|
87
|
+
if (run.pr !== null) tags.push(`pr:${run.pr}`, installTag(run));
|
|
95
88
|
if (run.headBranch) tags.push(`branch:${run.headBranch}`);
|
|
96
89
|
return tags;
|
|
97
90
|
};
|
|
98
91
|
|
|
99
92
|
/** The tag install commands are written against. */
|
|
100
93
|
export const installTag = (run: Run) =>
|
|
101
|
-
|
|
94
|
+
run.pr !== null ? `pr:${run.pr}:${run.headSha.slice(0, SHORT)}` : run.headSha.slice(0, SHORT);
|
|
102
95
|
|
|
103
96
|
const upstream = (e: { readonly _tag: string; readonly message?: string }) =>
|
|
104
97
|
new Upstream({ message: GitHub.describe(e) });
|
|
@@ -121,9 +114,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
121
114
|
if (!policy.repos.includes(ref.repo)) {
|
|
122
115
|
return yield* new Forbidden({ message: `${ref.repo} may not publish` });
|
|
123
116
|
}
|
|
124
|
-
const data = yield* github
|
|
125
|
-
.getRun(ref.repo, ref.runId)
|
|
126
|
-
.pipe(Effect.mapError(upstream));
|
|
117
|
+
const data = yield* github.getRun(ref.repo, ref.runId).pipe(Effect.mapError(upstream));
|
|
127
118
|
if (
|
|
128
119
|
data.status !== "in_progress" ||
|
|
129
120
|
(data.run_attempt !== undefined && data.run_attempt !== ref.attempt)
|
|
@@ -138,18 +129,23 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
138
129
|
const headRepo = data.head_repository?.full_name ?? data.repository.full_name;
|
|
139
130
|
let pr: number | null = null;
|
|
140
131
|
if (data.event === "pull_request") {
|
|
132
|
+
if (data.head_branch === null) {
|
|
133
|
+
return yield* new BadRequest({
|
|
134
|
+
message: `pull request run ${ref.runId} has no head branch`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
141
137
|
// The pull request whose head is this run's head, from the repository
|
|
142
138
|
// the run's head lives in. The same commit can head several pull
|
|
143
139
|
// requests, including one opened from a fork against a commit the
|
|
144
140
|
// repository already published; matching the head repository keeps a
|
|
145
141
|
// run's publication on its own pull request.
|
|
146
142
|
const pulls = yield* github
|
|
147
|
-
.pullRequestsForCommit(ref.repo, headRepo, data.head_sha)
|
|
143
|
+
.pullRequestsForCommit(ref.repo, headRepo, data.head_branch, data.head_sha)
|
|
148
144
|
.pipe(Effect.mapError(upstream));
|
|
149
145
|
const first = pulls[0];
|
|
150
146
|
if (first === undefined) {
|
|
151
147
|
return yield* new BadRequest({
|
|
152
|
-
message: `no pull request from ${headRepo} has head ${data.head_sha}`,
|
|
148
|
+
message: `no open pull request from ${headRepo} has head ${data.head_sha}`,
|
|
153
149
|
});
|
|
154
150
|
}
|
|
155
151
|
pr = first.number;
|
|
@@ -173,10 +169,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
173
169
|
* run, and GitHub reports the run's artifacts to the App, so a matching
|
|
174
170
|
* name means this run vouched for exactly this manifest text.
|
|
175
171
|
*/
|
|
176
|
-
const requireVouched = Effect.fn("requireVouched")(function* (
|
|
177
|
-
run: Run,
|
|
178
|
-
manifestText: string,
|
|
179
|
-
) {
|
|
172
|
+
const requireVouched = Effect.fn("requireVouched")(function* (run: Run, manifestText: string) {
|
|
180
173
|
const github = yield* GitHub.GitHubApp;
|
|
181
174
|
const expected = manifestArtifactName(yield* sha256(manifestText));
|
|
182
175
|
const artifacts = yield* github
|
|
@@ -189,22 +182,15 @@ const requireVouched = Effect.fn("requireVouched")(function* (
|
|
|
189
182
|
}
|
|
190
183
|
});
|
|
191
184
|
|
|
192
|
-
const publish = Effect.fn("publish")(function* (
|
|
193
|
-
run: Run,
|
|
194
|
-
manifestText: string,
|
|
195
|
-
) {
|
|
185
|
+
const publish = Effect.fn("publish")(function* (run: Run, manifestText: string) {
|
|
196
186
|
const { policy } = yield* RegistryConfig;
|
|
197
187
|
const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
|
|
198
188
|
const github = yield* GitHub.GitHubApp;
|
|
199
189
|
const base = yield* origin;
|
|
200
190
|
|
|
201
191
|
yield* requireVouched(run, manifestText);
|
|
202
|
-
const manifest = yield* Schema.decodeUnknownEffect(ManifestJson)(
|
|
203
|
-
|
|
204
|
-
).pipe(
|
|
205
|
-
Effect.mapError(
|
|
206
|
-
(e) => new BadRequest({ message: `invalid manifest: ${String(e)}` }),
|
|
207
|
-
),
|
|
192
|
+
const manifest = yield* Schema.decodeUnknownEffect(ManifestJson)(manifestText).pipe(
|
|
193
|
+
Effect.mapError((e) => new BadRequest({ message: `invalid manifest: ${String(e)}` })),
|
|
208
194
|
);
|
|
209
195
|
// The run vouched for the manifest, but the tags come from the run's
|
|
210
196
|
// head, so the packed checkout has to be that commit: a `pull_request`
|
|
@@ -218,9 +204,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
218
204
|
const packages = manifest.packages;
|
|
219
205
|
const maxSize = policy.maxPackageSize;
|
|
220
206
|
const tooLarge =
|
|
221
|
-
maxSize === undefined
|
|
222
|
-
? undefined
|
|
223
|
-
: packages.find((pkg) => BigInt(pkg.size) > maxSize);
|
|
207
|
+
maxSize === undefined ? undefined : packages.find((pkg) => BigInt(pkg.size) > maxSize);
|
|
224
208
|
if (tooLarge !== undefined) {
|
|
225
209
|
return yield* new PackageTooLarge({
|
|
226
210
|
message: `${tooLarge.name} exceeds ${maxSize} bytes`,
|
|
@@ -230,15 +214,9 @@ const publish = Effect.fn("publish")(function* (
|
|
|
230
214
|
const missing = yield* Effect.filter(
|
|
231
215
|
packages,
|
|
232
216
|
(pkg) =>
|
|
233
|
-
r2
|
|
234
|
-
.head(tarballKey(pkg.name, pkg.sha256))
|
|
235
|
-
.pipe(Effect.map((object) => object === null)),
|
|
217
|
+
r2.head(tarballKey(pkg.name, pkg.sha256)).pipe(Effect.map((object) => object === null)),
|
|
236
218
|
{ concurrency: 8 },
|
|
237
|
-
).pipe(
|
|
238
|
-
Effect.map((packages) =>
|
|
239
|
-
packages.map(({ name, sha256 }) => ({ name, sha256 })),
|
|
240
|
-
),
|
|
241
|
-
);
|
|
219
|
+
).pipe(Effect.map((packages) => packages.map(({ name, sha256 }) => ({ name, sha256 }))));
|
|
242
220
|
if (missing.length > 0) {
|
|
243
221
|
return yield* new MissingTarballs({ missing });
|
|
244
222
|
}
|
|
@@ -285,9 +263,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
285
263
|
})
|
|
286
264
|
.pipe(
|
|
287
265
|
Effect.catch((e) =>
|
|
288
|
-
Effect.logWarning(
|
|
289
|
-
`check run on ${run.repo}@${run.headSha} failed: ${GitHub.describe(e)}`,
|
|
290
|
-
),
|
|
266
|
+
Effect.logWarning(`check run on ${run.repo}@${run.headSha} failed: ${GitHub.describe(e)}`),
|
|
291
267
|
),
|
|
292
268
|
);
|
|
293
269
|
if (run.pr !== null) {
|
|
@@ -299,9 +275,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
299
275
|
.upsertComment(run.repo, run.pr, COMMENT_MARKER, body)
|
|
300
276
|
.pipe(
|
|
301
277
|
Effect.catch((e) =>
|
|
302
|
-
Effect.logWarning(
|
|
303
|
-
`comment on ${run.repo}#${run.pr} failed: ${GitHub.describe(e)}`,
|
|
304
|
-
),
|
|
278
|
+
Effect.logWarning(`comment on ${run.repo}#${run.pr} failed: ${GitHub.describe(e)}`),
|
|
305
279
|
),
|
|
306
280
|
);
|
|
307
281
|
}
|
|
@@ -339,13 +313,9 @@ const uploadTarball = Effect.fn("uploadTarball")(function* (
|
|
|
339
313
|
.put(key, request.stream, {
|
|
340
314
|
contentLength,
|
|
341
315
|
// The params schema already constrained this to 64 hex chars.
|
|
342
|
-
sha256: Result.getOrThrow(
|
|
316
|
+
sha256: Result.getOrThrow(Hex.decode(sha256)),
|
|
343
317
|
})
|
|
344
|
-
.pipe(
|
|
345
|
-
Effect.mapError(
|
|
346
|
-
(e) => new BadRequest({ message: `upload rejected: ${String(e)}` }),
|
|
347
|
-
),
|
|
348
|
-
);
|
|
318
|
+
.pipe(Effect.mapError((e) => new BadRequest({ message: `upload rejected: ${String(e)}` })));
|
|
349
319
|
return { name, sha256, size: contentLength, uploaded: true };
|
|
350
320
|
});
|
|
351
321
|
|
|
@@ -368,9 +338,7 @@ export const parseInstallPath = (
|
|
|
368
338
|
// Malformed percent-encoding is a path nothing can be published under.
|
|
369
339
|
return undefined;
|
|
370
340
|
}
|
|
371
|
-
const tarball = path.match(
|
|
372
|
-
/^(@[^/]+\/[^/@]+|[^/@]+)\/-\/([a-f0-9]{64})\.tgz$/,
|
|
373
|
-
);
|
|
341
|
+
const tarball = path.match(/^(@[^/]+\/[^/@]+|[^/@]+)\/-\/([a-f0-9]{64})\.tgz$/);
|
|
374
342
|
if (tarball) {
|
|
375
343
|
return {
|
|
376
344
|
kind: "tarball",
|
|
@@ -391,8 +359,7 @@ export const parseInstallPath = (
|
|
|
391
359
|
const qualify = (name: string, scope: string | undefined) =>
|
|
392
360
|
scope !== undefined && !name.startsWith("@") ? `${scope}/${name}` : name;
|
|
393
361
|
|
|
394
|
-
const notFound = (message: string) =>
|
|
395
|
-
HttpServerResponse.json({ error: message }, { status: 404 });
|
|
362
|
+
const notFound = (message: string) => HttpServerResponse.json({ error: message }, { status: 404 });
|
|
396
363
|
|
|
397
364
|
/**
|
|
398
365
|
* Install URLs: `/<name>/<tag>` redirects to the immutable tarball URL,
|
|
@@ -405,16 +372,10 @@ const install = Effect.gen(function* () {
|
|
|
405
372
|
const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
|
|
406
373
|
|
|
407
374
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
408
|
-
return yield* HttpServerResponse.json(
|
|
409
|
-
{ error: "method not allowed" },
|
|
410
|
-
{ status: 405 },
|
|
411
|
-
);
|
|
375
|
+
return yield* HttpServerResponse.json({ error: "method not allowed" }, { status: 405 });
|
|
412
376
|
}
|
|
413
377
|
const host = request.headers.host ?? "localhost";
|
|
414
|
-
const target = parseInstallPath(
|
|
415
|
-
new URL(request.url, `https://${host}`).pathname,
|
|
416
|
-
aliases[host],
|
|
417
|
-
);
|
|
378
|
+
const target = parseInstallPath(new URL(request.url, `https://${host}`).pathname, aliases[host]);
|
|
418
379
|
if (target === undefined) {
|
|
419
380
|
return yield* notFound("not found");
|
|
420
381
|
}
|
|
@@ -444,7 +405,7 @@ const install = Effect.gen(function* () {
|
|
|
444
405
|
|
|
445
406
|
const banner = Effect.map(origin, (base) =>
|
|
446
407
|
HttpServerResponse.text(
|
|
447
|
-
`Preview package registry. Install with: bun add ${base}/<package>/<commit|branch:name|pr:N>\n`,
|
|
408
|
+
`Preview package registry. Install with: bun add ${base}/<package>/<commit|branch:name|pr:N|pr:N:short-sha>\n`,
|
|
448
409
|
),
|
|
449
410
|
);
|
|
450
411
|
|
|
@@ -478,9 +439,7 @@ export const handler = Effect.gen(function* () {
|
|
|
478
439
|
|
|
479
440
|
// Built once per isolate. The D1 client memoizes its connection on each
|
|
480
441
|
// event's scope, so its layer is safe to share.
|
|
481
|
-
const github = yield* GitHub.GitHubApp.pipe(
|
|
482
|
-
Effect.provide(GitHub.GitHubAppLive),
|
|
483
|
-
);
|
|
442
|
+
const github = yield* GitHub.GitHubApp.pipe(Effect.provide(GitHub.GitHubAppLive));
|
|
484
443
|
const services = Layer.mergeAll(
|
|
485
444
|
Cloudflare.R2.ReadWriteBucketBinding,
|
|
486
445
|
Cloudflare.KV.ReadWriteNamespaceBinding,
|
|
@@ -497,23 +456,20 @@ export const handler = Effect.gen(function* () {
|
|
|
497
456
|
(ref) => Effect.map(lookupRun(ref), (value) => ({ value, ttl: RUN_CACHE })),
|
|
498
457
|
);
|
|
499
458
|
|
|
500
|
-
yield* Cloudflare.Workers.cron(config.cron, () =>
|
|
501
|
-
sweep.pipe(Effect.provide(services)),
|
|
502
|
-
);
|
|
459
|
+
yield* Cloudflare.Workers.cron(config.cron, () => sweep.pipe(Effect.provide(services)));
|
|
503
460
|
|
|
504
461
|
const registry = HttpApiBuilder.group(PkgApi, "Registry", (handlers) =>
|
|
505
462
|
handlers
|
|
506
463
|
.handle("publish", ({ payload }) =>
|
|
507
|
-
Effect.flatMap(runs(payload.run), (run) =>
|
|
508
|
-
|
|
509
|
-
)
|
|
464
|
+
Effect.flatMap(runs(payload.run), (run) => publish(run, payload.manifest)).pipe(
|
|
465
|
+
Effect.catchTag(storageFailures, (e) => Effect.die(e)),
|
|
466
|
+
),
|
|
510
467
|
)
|
|
511
468
|
// Raw so the body streams into R2 instead of being buffered.
|
|
512
469
|
.handleRaw("uploadTarball", ({ query, params, request }) =>
|
|
513
|
-
Effect.andThen(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
).pipe(Effect.catchTag("R2Error", (e) => Effect.die(e))),
|
|
470
|
+
Effect.andThen(runs(query), uploadTarball(request, params.name, params.sha256)).pipe(
|
|
471
|
+
Effect.catchTag("R2Error", (e) => Effect.die(e)),
|
|
472
|
+
),
|
|
517
473
|
),
|
|
518
474
|
);
|
|
519
475
|
|
package/src/Registry/KVCache.ts
CHANGED
|
@@ -7,8 +7,7 @@ import { Cache } from "./Bindings.ts";
|
|
|
7
7
|
/** KV's floor for both `expirationTtl` and `cacheTtl`. */
|
|
8
8
|
const MIN_TTL = Duration.minutes(1);
|
|
9
9
|
|
|
10
|
-
const seconds = (duration: Duration.Duration) =>
|
|
11
|
-
Math.floor(Duration.toSeconds(duration));
|
|
10
|
+
const seconds = (duration: Duration.Duration) => Math.floor(Duration.toSeconds(duration));
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* A lookup-through cache on the registry's KV namespace, shared by every
|
|
@@ -28,9 +27,7 @@ const seconds = (duration: Duration.Duration) =>
|
|
|
28
27
|
export const make = <K, A, I, E, R>(
|
|
29
28
|
key: (input: K) => string,
|
|
30
29
|
value: Schema.Codec<A, I>,
|
|
31
|
-
lookup: (
|
|
32
|
-
input: K,
|
|
33
|
-
) => Effect.Effect<{ value: A; ttl: Duration.Duration }, E, R>,
|
|
30
|
+
lookup: (input: K) => Effect.Effect<{ value: A; ttl: Duration.Duration }, E, R>,
|
|
34
31
|
) => {
|
|
35
32
|
const json = Schema.fromJsonString(value);
|
|
36
33
|
const decode = Schema.decodeUnknownEffect(json);
|
|
@@ -40,13 +37,9 @@ export const make = <K, A, I, E, R>(
|
|
|
40
37
|
const kv = yield* Cloudflare.KV.ReadWriteNamespace(Cache);
|
|
41
38
|
const id = key(input);
|
|
42
39
|
const hit = yield* kv.get(id, { cacheTtl: seconds(MIN_TTL) }).pipe(
|
|
43
|
-
Effect.flatMap((text) =>
|
|
44
|
-
text === null ? Effect.succeed(undefined) : decode(text),
|
|
45
|
-
),
|
|
40
|
+
Effect.flatMap((text) => (text === null ? Effect.succeed(undefined) : decode(text))),
|
|
46
41
|
Effect.catch((e) =>
|
|
47
|
-
Effect.logWarning(`cache read of ${id} failed: ${e}`).pipe(
|
|
48
|
-
Effect.as(undefined),
|
|
49
|
-
),
|
|
42
|
+
Effect.logWarning(`cache read of ${id} failed: ${e}`).pipe(Effect.as(undefined)),
|
|
50
43
|
),
|
|
51
44
|
);
|
|
52
45
|
if (hit !== undefined) {
|
|
@@ -55,12 +48,8 @@ export const make = <K, A, I, E, R>(
|
|
|
55
48
|
const fresh = yield* lookup(input);
|
|
56
49
|
if (Duration.isGreaterThanOrEqualTo(fresh.ttl, MIN_TTL)) {
|
|
57
50
|
yield* encode(fresh.value).pipe(
|
|
58
|
-
Effect.flatMap((text) =>
|
|
59
|
-
|
|
60
|
-
),
|
|
61
|
-
Effect.catch((e) =>
|
|
62
|
-
Effect.logWarning(`cache write of ${id} failed: ${e}`),
|
|
63
|
-
),
|
|
51
|
+
Effect.flatMap((text) => kv.put(id, text, { expirationTtl: seconds(fresh.ttl) })),
|
|
52
|
+
Effect.catch((e) => Effect.logWarning(`cache write of ${id} failed: ${e}`)),
|
|
64
53
|
);
|
|
65
54
|
}
|
|
66
55
|
return fresh.value;
|
package/src/Registry/Sweep.ts
CHANGED
|
@@ -4,13 +4,7 @@ import * as Duration from "effect/Duration";
|
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Option from "effect/Option";
|
|
6
6
|
import * as Stream from "effect/Stream";
|
|
7
|
-
import {
|
|
8
|
-
Bucket,
|
|
9
|
-
ORPHAN_GRACE,
|
|
10
|
-
RegistryConfig,
|
|
11
|
-
SWEEP_LOOKAHEAD,
|
|
12
|
-
tarballKey,
|
|
13
|
-
} from "./Bindings.ts";
|
|
7
|
+
import { Bucket, ORPHAN_GRACE, RegistryConfig, SWEEP_LOOKAHEAD, tarballKey } from "./Bindings.ts";
|
|
14
8
|
import * as GitHub from "./GitHub.ts";
|
|
15
9
|
import * as Tags from "./Tags.ts";
|
|
16
10
|
|
|
@@ -37,15 +31,13 @@ export const sweep = Effect.gen(function* () {
|
|
|
37
31
|
const [repo, number] = ref.split("#");
|
|
38
32
|
return github.getPullRequest(repo!, Number(number)).pipe(
|
|
39
33
|
Effect.map((pr) =>
|
|
40
|
-
pr.state === "open"
|
|
41
|
-
? undefined
|
|
42
|
-
: Date.parse(pr.merged_at ?? pr.closed_at ?? "") || now,
|
|
34
|
+
pr.state === "open" ? undefined : Date.parse(pr.merged_at ?? pr.closed_at ?? "") || now,
|
|
43
35
|
),
|
|
44
36
|
Effect.map((closedAt) => [ref, { closedAt }] as const),
|
|
45
37
|
Effect.catch((e) =>
|
|
46
|
-
Effect.logWarning(
|
|
47
|
-
|
|
48
|
-
)
|
|
38
|
+
Effect.logWarning(`pull request ${ref} lookup failed: ${GitHub.describe(e)}`).pipe(
|
|
39
|
+
Effect.as([ref, undefined] as const),
|
|
40
|
+
),
|
|
49
41
|
),
|
|
50
42
|
);
|
|
51
43
|
},
|
|
@@ -85,20 +77,15 @@ export const sweep = Effect.gen(function* () {
|
|
|
85
77
|
);
|
|
86
78
|
|
|
87
79
|
// Uploads that never got tagged.
|
|
88
|
-
const orphans = yield* Stream.paginate(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(page) =>
|
|
96
|
-
[
|
|
97
|
-
page.objects,
|
|
98
|
-
page.truncated ? Option.some(page.cursor) : Option.none(),
|
|
99
|
-
] as const,
|
|
100
|
-
),
|
|
80
|
+
const orphans = yield* Stream.paginate(undefined as string | undefined, (cursor) =>
|
|
81
|
+
r2
|
|
82
|
+
.list({ cursor, limit: 500 })
|
|
83
|
+
.pipe(
|
|
84
|
+
Effect.map(
|
|
85
|
+
(page) =>
|
|
86
|
+
[page.objects, page.truncated ? Option.some(page.cursor) : Option.none()] as const,
|
|
101
87
|
),
|
|
88
|
+
),
|
|
102
89
|
).pipe(
|
|
103
90
|
Stream.filter((object) => {
|
|
104
91
|
const match = object.key.match(/^(.+)\/([a-f0-9]{64})\.tgz$/);
|