@alchemy.run/pkg 2.0.0-beta.80 → 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/dist/Manifest.d.ts.map +1 -1
- package/dist/Manifest.js.map +1 -1
- package/dist/Protocol.d.ts +1 -1
- package/dist/Protocol.d.ts.map +1 -1
- package/dist/Protocol.js +3 -12
- 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 +2 -2
- package/dist/Registry/GitHub.d.ts.map +1 -1
- package/dist/Registry/GitHub.js +9 -36
- package/dist/Registry/GitHub.js.map +1 -1
- package/dist/Registry/Handler.d.ts +1 -1
- package/dist/Registry/Handler.d.ts.map +1 -1
- package/dist/Registry/Handler.js +10 -18
- 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.map +1 -1
- package/dist/Registry/Sweep.js +3 -8
- package/dist/Registry/Sweep.js.map +1 -1
- package/dist/Registry/Tags.d.ts.map +1 -1
- package/dist/Registry/Tags.js.map +1 -1
- package/dist/Registry.d.ts +1 -1
- 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 +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +5 -9
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +11 -23
- package/dist/cli/pack.js.map +1 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/publish.js +2 -2
- package/dist/cli/publish.js.map +1 -1
- package/package.json +19 -19
- package/src/Manifest.ts +1 -3
- package/src/Protocol.ts +12 -28
- package/src/Registry/Bindings.ts +1 -4
- package/src/Registry/GitHub.ts +24 -83
- package/src/Registry/Handler.ts +31 -87
- package/src/Registry/KVCache.ts +6 -17
- package/src/Registry/Sweep.ts +13 -26
- package/src/Registry/Tags.ts +7 -15
- package/src/Registry.ts +5 -15
- package/src/cli/bin.ts +8 -18
- package/src/cli/pack.ts +27 -73
- package/src/cli/publish.ts +11 -23
package/src/Registry/GitHub.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -12,19 +13,16 @@ import * as DateTime from "effect/DateTime";
|
|
|
12
13
|
import * as Duration from "effect/Duration";
|
|
13
14
|
import * as Effect from "effect/Effect";
|
|
14
15
|
import { Base64Url } from "effect/encoding";
|
|
16
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
15
17
|
import * as Layer from "effect/Layer";
|
|
16
18
|
import * as Option from "effect/Option";
|
|
17
19
|
import * as Redacted from "effect/Redacted";
|
|
18
20
|
import * as Schema from "effect/Schema";
|
|
19
21
|
import * as Stream from "effect/Stream";
|
|
20
|
-
import * as HttpClient from "effect/http/HttpClient";
|
|
21
|
-
import * as crypto from "node:crypto";
|
|
22
22
|
import { COMMENT_MARKER, RegistryConfig } from "./Bindings.ts";
|
|
23
23
|
import * as KVCache from "./KVCache.ts";
|
|
24
24
|
|
|
25
|
-
export class CryptoError extends Data.TaggedError("CryptoError")<{
|
|
26
|
-
readonly message: string;
|
|
27
|
-
}> {}
|
|
25
|
+
export class CryptoError extends Data.TaggedError("CryptoError")<{ readonly message: string }> {}
|
|
28
26
|
|
|
29
27
|
export class AppNotInstalled extends Data.TaggedError("AppNotInstalled")<{
|
|
30
28
|
readonly repo: string;
|
|
@@ -38,20 +36,14 @@ export class AppNotInstalled extends Data.TaggedError("AppNotInstalled")<{
|
|
|
38
36
|
export const importPrivateKey = (pem: string) =>
|
|
39
37
|
Effect.try({
|
|
40
38
|
try: () => crypto.createPrivateKey({ key: pem }),
|
|
41
|
-
catch: (cause) =>
|
|
42
|
-
new CryptoError({ message: `invalid private key: ${cause}` }),
|
|
39
|
+
catch: (cause) => new CryptoError({ message: `invalid private key: ${cause}` }),
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
/** Sign a compact RS256 JWT, used to authenticate as the GitHub App. */
|
|
46
|
-
export const signJwt = (
|
|
47
|
-
claims: Record<string, unknown>,
|
|
48
|
-
key: crypto.KeyObject,
|
|
49
|
-
) =>
|
|
43
|
+
export const signJwt = (claims: Record<string, unknown>, key: crypto.KeyObject) =>
|
|
50
44
|
Effect.try({
|
|
51
45
|
try: () => {
|
|
52
|
-
const header = Base64Url.encode(
|
|
53
|
-
JSON.stringify({ alg: "RS256", typ: "JWT" }),
|
|
54
|
-
);
|
|
46
|
+
const header = Base64Url.encode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
55
47
|
const payload = Base64Url.encode(JSON.stringify(claims));
|
|
56
48
|
const input = `${header}.${payload}`;
|
|
57
49
|
const signature = crypto.sign("sha256", Buffer.from(input), key);
|
|
@@ -69,21 +61,14 @@ export const renderInstalls = (
|
|
|
69
61
|
packages: ReadonlyArray<{ name: string; group: string; url: string }>,
|
|
70
62
|
groups: ReadonlyArray<{ name: string; collapsed: boolean }>,
|
|
71
63
|
) => {
|
|
72
|
-
const collapsed = new Set(
|
|
73
|
-
groups.filter((g) => g.collapsed).map((g) => g.name),
|
|
74
|
-
);
|
|
64
|
+
const collapsed = new Set(groups.filter((g) => g.collapsed).map((g) => g.name));
|
|
75
65
|
// One code block per package so each command has its own copy button, in
|
|
76
66
|
// the order the manifest lists them, which is the order they were given
|
|
77
67
|
// to `pkg pack`. Blank lines around the markdown inside `<details>` are
|
|
78
68
|
// what make GitHub render it.
|
|
79
69
|
return Object.entries(Arr.groupBy(packages, (pkg) => pkg.group))
|
|
80
70
|
.flatMap(([group, members]) => {
|
|
81
|
-
const installs = members.flatMap(({ url }) => [
|
|
82
|
-
"```sh",
|
|
83
|
-
`pnpm install ${url}`,
|
|
84
|
-
"```",
|
|
85
|
-
"",
|
|
86
|
-
]);
|
|
71
|
+
const installs = members.flatMap(({ url }) => ["```sh", `pnpm install ${url}`, "```", ""]);
|
|
87
72
|
return collapsed.has(group)
|
|
88
73
|
? [
|
|
89
74
|
"<details>",
|
|
@@ -177,10 +162,7 @@ const make = Effect.gen(function* () {
|
|
|
177
162
|
const pem = yield* config.github.privateKey;
|
|
178
163
|
const key = yield* importPrivateKey(Redacted.value(pem));
|
|
179
164
|
const now = Math.floor((yield* Clock.currentTimeMillis) / 1000);
|
|
180
|
-
return yield* signJwt(
|
|
181
|
-
{ iat: now - 60, exp: now + 540, iss: yield* config.github.appId },
|
|
182
|
-
key,
|
|
183
|
-
);
|
|
165
|
+
return yield* signJwt({ iat: now - 60, exp: now + 540, iss: yield* config.github.appId }, key);
|
|
184
166
|
});
|
|
185
167
|
|
|
186
168
|
// Failures are not kept: the next call retries the mint.
|
|
@@ -190,17 +172,12 @@ const make = Effect.gen(function* () {
|
|
|
190
172
|
(repo) =>
|
|
191
173
|
Effect.gen(function* () {
|
|
192
174
|
const jwt = yield* appJwt;
|
|
193
|
-
const installation = yield* as(
|
|
194
|
-
jwt,
|
|
195
|
-
Apps.getRepoInstallation(split(repo)),
|
|
196
|
-
).pipe(
|
|
175
|
+
const installation = yield* as(jwt, Apps.getRepoInstallation(split(repo))).pipe(
|
|
197
176
|
Effect.catchTag("NotFound", () => new AppNotInstalled({ repo })),
|
|
198
177
|
);
|
|
199
178
|
const access = yield* as(
|
|
200
179
|
jwt,
|
|
201
|
-
Apps.createInstallationAccessToken({
|
|
202
|
-
installation_id: installation.id,
|
|
203
|
-
}),
|
|
180
|
+
Apps.createInstallationAccessToken({ installation_id: installation.id }),
|
|
204
181
|
);
|
|
205
182
|
const now = yield* Clock.currentTimeMillis;
|
|
206
183
|
return {
|
|
@@ -214,17 +191,12 @@ const make = Effect.gen(function* () {
|
|
|
214
191
|
);
|
|
215
192
|
|
|
216
193
|
/** A GitHub operation run as the App's installation on `repo`. */
|
|
217
|
-
const asInstallation = <A, E, R>(
|
|
218
|
-
repo
|
|
219
|
-
operation: Effect.Effect<A, E, R>,
|
|
220
|
-
) => 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));
|
|
221
196
|
|
|
222
197
|
return {
|
|
223
198
|
getRun: (repo: string, runId: number) =>
|
|
224
|
-
asInstallation(
|
|
225
|
-
repo,
|
|
226
|
-
Actions.getWorkflowRun({ ...split(repo), run_id: runId }),
|
|
227
|
-
),
|
|
199
|
+
asInstallation(repo, Actions.getWorkflowRun({ ...split(repo), run_id: runId })),
|
|
228
200
|
|
|
229
201
|
/**
|
|
230
202
|
* Artifacts named `name` uploaded to a run so far, including by jobs
|
|
@@ -233,12 +205,7 @@ const make = Effect.gen(function* () {
|
|
|
233
205
|
listRunArtifacts: (repo: string, runId: number, name: string) =>
|
|
234
206
|
asInstallation(
|
|
235
207
|
repo,
|
|
236
|
-
Actions.listWorkflowRunArtifacts({
|
|
237
|
-
...split(repo),
|
|
238
|
-
run_id: runId,
|
|
239
|
-
name,
|
|
240
|
-
per_page: 100,
|
|
241
|
-
}),
|
|
208
|
+
Actions.listWorkflowRunArtifacts({ ...split(repo), run_id: runId, name, per_page: 100 }),
|
|
242
209
|
).pipe(Effect.map((page) => page.artifacts)),
|
|
243
210
|
|
|
244
211
|
/**
|
|
@@ -246,12 +213,7 @@ const make = Effect.gen(function* () {
|
|
|
246
213
|
* repository the commit was pushed to. Commit-associated PR lookups can
|
|
247
214
|
* return no results for forks, so discover candidates by owner and branch.
|
|
248
215
|
*/
|
|
249
|
-
pullRequestsForCommit: (
|
|
250
|
-
repo: string,
|
|
251
|
-
headRepo: string,
|
|
252
|
-
headBranch: string,
|
|
253
|
-
sha: string,
|
|
254
|
-
) =>
|
|
216
|
+
pullRequestsForCommit: (repo: string, headRepo: string, headBranch: string, sha: string) =>
|
|
255
217
|
asInstallation(
|
|
256
218
|
repo,
|
|
257
219
|
Pulls.list({
|
|
@@ -309,28 +271,16 @@ const make = Effect.gen(function* () {
|
|
|
309
271
|
* Create or update the comment on `issue` whose body starts with
|
|
310
272
|
* `marker`, looking through the first five pages of comments.
|
|
311
273
|
*/
|
|
312
|
-
upsertComment: (
|
|
313
|
-
repo: string,
|
|
314
|
-
issue: number,
|
|
315
|
-
marker: string,
|
|
316
|
-
body: string,
|
|
317
|
-
) =>
|
|
274
|
+
upsertComment: (repo: string, issue: number, marker: string, body: string) =>
|
|
318
275
|
Effect.gen(function* () {
|
|
319
276
|
const comments = Stream.paginate(1, (page) =>
|
|
320
277
|
asInstallation(
|
|
321
278
|
repo,
|
|
322
|
-
Issues.listComments({
|
|
323
|
-
...split(repo),
|
|
324
|
-
issue_number: issue,
|
|
325
|
-
per_page: 100,
|
|
326
|
-
page,
|
|
327
|
-
}),
|
|
279
|
+
Issues.listComments({ ...split(repo), issue_number: issue, per_page: 100, page }),
|
|
328
280
|
).pipe(
|
|
329
281
|
Effect.map((comments) => [
|
|
330
282
|
comments,
|
|
331
|
-
comments.length < 100 || page >= 5
|
|
332
|
-
? Option.none()
|
|
333
|
-
: Option.some(page + 1),
|
|
283
|
+
comments.length < 100 || page >= 5 ? Option.none() : Option.some(page + 1),
|
|
334
284
|
]),
|
|
335
285
|
),
|
|
336
286
|
);
|
|
@@ -344,29 +294,20 @@ const make = Effect.gen(function* () {
|
|
|
344
294
|
onNone: () =>
|
|
345
295
|
asInstallation(
|
|
346
296
|
repo,
|
|
347
|
-
Issues.createComment({
|
|
348
|
-
...split(repo),
|
|
349
|
-
issue_number: issue,
|
|
350
|
-
body,
|
|
351
|
-
}),
|
|
297
|
+
Issues.createComment({ ...split(repo), issue_number: issue, body }),
|
|
352
298
|
),
|
|
353
299
|
onSome: (comment) =>
|
|
354
300
|
asInstallation(
|
|
355
301
|
repo,
|
|
356
|
-
Issues.updateComment({
|
|
357
|
-
...split(repo),
|
|
358
|
-
comment_id: comment.id,
|
|
359
|
-
body,
|
|
360
|
-
}),
|
|
302
|
+
Issues.updateComment({ ...split(repo), comment_id: comment.id, body }),
|
|
361
303
|
),
|
|
362
304
|
});
|
|
363
305
|
}),
|
|
364
306
|
};
|
|
365
307
|
});
|
|
366
308
|
|
|
367
|
-
export class GitHubApp extends Context.Service<
|
|
368
|
-
GitHubApp,
|
|
369
|
-
|
|
370
|
-
>()("@alchemy.run/pkg/GitHubApp") {}
|
|
309
|
+
export class GitHubApp extends Context.Service<GitHubApp, Effect.Success<typeof make>>()(
|
|
310
|
+
"@alchemy.run/pkg/GitHubApp",
|
|
311
|
+
) {}
|
|
371
312
|
|
|
372
313
|
export const GitHubAppLive = Layer.effect(GitHubApp, make);
|
package/src/Registry/Handler.ts
CHANGED
|
@@ -5,17 +5,17 @@ import * as Clock from "effect/Clock";
|
|
|
5
5
|
import * as Duration from "effect/Duration";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import { Hex } from "effect/encoding";
|
|
8
|
-
import * as
|
|
9
|
-
import * as Path from "effect/Path";
|
|
10
|
-
import * as Result from "effect/Result";
|
|
11
|
-
import * as Schema from "effect/Schema";
|
|
8
|
+
import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
|
|
12
9
|
import * as Etag from "effect/http/Etag";
|
|
13
10
|
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
14
11
|
import * as HttpPlatform from "effect/http/HttpPlatform";
|
|
15
12
|
import * as HttpRouter from "effect/http/HttpRouter";
|
|
16
13
|
import { HttpServerRequest } from "effect/http/HttpServerRequest";
|
|
17
14
|
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
18
|
-
import * as
|
|
15
|
+
import * as Layer from "effect/Layer";
|
|
16
|
+
import * as Path from "effect/Path";
|
|
17
|
+
import * as Result from "effect/Result";
|
|
18
|
+
import * as Schema from "effect/Schema";
|
|
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";
|
|
@@ -98,9 +91,7 @@ export const tagsFor = (run: Run): string[] => {
|
|
|
98
91
|
|
|
99
92
|
/** The tag install commands are written against. */
|
|
100
93
|
export const installTag = (run: Run) =>
|
|
101
|
-
run.pr !== null
|
|
102
|
-
? `pr:${run.pr}:${run.headSha.slice(0, SHORT)}`
|
|
103
|
-
: run.headSha.slice(0, SHORT);
|
|
94
|
+
run.pr !== null ? `pr:${run.pr}:${run.headSha.slice(0, SHORT)}` : run.headSha.slice(0, SHORT);
|
|
104
95
|
|
|
105
96
|
const upstream = (e: { readonly _tag: string; readonly message?: string }) =>
|
|
106
97
|
new Upstream({ message: GitHub.describe(e) });
|
|
@@ -123,9 +114,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
123
114
|
if (!policy.repos.includes(ref.repo)) {
|
|
124
115
|
return yield* new Forbidden({ message: `${ref.repo} may not publish` });
|
|
125
116
|
}
|
|
126
|
-
const data = yield* github
|
|
127
|
-
.getRun(ref.repo, ref.runId)
|
|
128
|
-
.pipe(Effect.mapError(upstream));
|
|
117
|
+
const data = yield* github.getRun(ref.repo, ref.runId).pipe(Effect.mapError(upstream));
|
|
129
118
|
if (
|
|
130
119
|
data.status !== "in_progress" ||
|
|
131
120
|
(data.run_attempt !== undefined && data.run_attempt !== ref.attempt)
|
|
@@ -151,12 +140,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
151
140
|
// repository already published; matching the head repository keeps a
|
|
152
141
|
// run's publication on its own pull request.
|
|
153
142
|
const pulls = yield* github
|
|
154
|
-
.pullRequestsForCommit(
|
|
155
|
-
ref.repo,
|
|
156
|
-
headRepo,
|
|
157
|
-
data.head_branch,
|
|
158
|
-
data.head_sha,
|
|
159
|
-
)
|
|
143
|
+
.pullRequestsForCommit(ref.repo, headRepo, data.head_branch, data.head_sha)
|
|
160
144
|
.pipe(Effect.mapError(upstream));
|
|
161
145
|
const first = pulls[0];
|
|
162
146
|
if (first === undefined) {
|
|
@@ -185,10 +169,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
185
169
|
* run, and GitHub reports the run's artifacts to the App, so a matching
|
|
186
170
|
* name means this run vouched for exactly this manifest text.
|
|
187
171
|
*/
|
|
188
|
-
const requireVouched = Effect.fn("requireVouched")(function* (
|
|
189
|
-
run: Run,
|
|
190
|
-
manifestText: string,
|
|
191
|
-
) {
|
|
172
|
+
const requireVouched = Effect.fn("requireVouched")(function* (run: Run, manifestText: string) {
|
|
192
173
|
const github = yield* GitHub.GitHubApp;
|
|
193
174
|
const expected = manifestArtifactName(yield* sha256(manifestText));
|
|
194
175
|
const artifacts = yield* github
|
|
@@ -201,22 +182,15 @@ const requireVouched = Effect.fn("requireVouched")(function* (
|
|
|
201
182
|
}
|
|
202
183
|
});
|
|
203
184
|
|
|
204
|
-
const publish = Effect.fn("publish")(function* (
|
|
205
|
-
run: Run,
|
|
206
|
-
manifestText: string,
|
|
207
|
-
) {
|
|
185
|
+
const publish = Effect.fn("publish")(function* (run: Run, manifestText: string) {
|
|
208
186
|
const { policy } = yield* RegistryConfig;
|
|
209
187
|
const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
|
|
210
188
|
const github = yield* GitHub.GitHubApp;
|
|
211
189
|
const base = yield* origin;
|
|
212
190
|
|
|
213
191
|
yield* requireVouched(run, manifestText);
|
|
214
|
-
const manifest = yield* Schema.decodeUnknownEffect(ManifestJson)(
|
|
215
|
-
|
|
216
|
-
).pipe(
|
|
217
|
-
Effect.mapError(
|
|
218
|
-
(e) => new BadRequest({ message: `invalid manifest: ${String(e)}` }),
|
|
219
|
-
),
|
|
192
|
+
const manifest = yield* Schema.decodeUnknownEffect(ManifestJson)(manifestText).pipe(
|
|
193
|
+
Effect.mapError((e) => new BadRequest({ message: `invalid manifest: ${String(e)}` })),
|
|
220
194
|
);
|
|
221
195
|
// The run vouched for the manifest, but the tags come from the run's
|
|
222
196
|
// head, so the packed checkout has to be that commit: a `pull_request`
|
|
@@ -230,9 +204,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
230
204
|
const packages = manifest.packages;
|
|
231
205
|
const maxSize = policy.maxPackageSize;
|
|
232
206
|
const tooLarge =
|
|
233
|
-
maxSize === undefined
|
|
234
|
-
? undefined
|
|
235
|
-
: packages.find((pkg) => BigInt(pkg.size) > maxSize);
|
|
207
|
+
maxSize === undefined ? undefined : packages.find((pkg) => BigInt(pkg.size) > maxSize);
|
|
236
208
|
if (tooLarge !== undefined) {
|
|
237
209
|
return yield* new PackageTooLarge({
|
|
238
210
|
message: `${tooLarge.name} exceeds ${maxSize} bytes`,
|
|
@@ -242,15 +214,9 @@ const publish = Effect.fn("publish")(function* (
|
|
|
242
214
|
const missing = yield* Effect.filter(
|
|
243
215
|
packages,
|
|
244
216
|
(pkg) =>
|
|
245
|
-
r2
|
|
246
|
-
.head(tarballKey(pkg.name, pkg.sha256))
|
|
247
|
-
.pipe(Effect.map((object) => object === null)),
|
|
217
|
+
r2.head(tarballKey(pkg.name, pkg.sha256)).pipe(Effect.map((object) => object === null)),
|
|
248
218
|
{ concurrency: 8 },
|
|
249
|
-
).pipe(
|
|
250
|
-
Effect.map((packages) =>
|
|
251
|
-
packages.map(({ name, sha256 }) => ({ name, sha256 })),
|
|
252
|
-
),
|
|
253
|
-
);
|
|
219
|
+
).pipe(Effect.map((packages) => packages.map(({ name, sha256 }) => ({ name, sha256 }))));
|
|
254
220
|
if (missing.length > 0) {
|
|
255
221
|
return yield* new MissingTarballs({ missing });
|
|
256
222
|
}
|
|
@@ -297,9 +263,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
297
263
|
})
|
|
298
264
|
.pipe(
|
|
299
265
|
Effect.catch((e) =>
|
|
300
|
-
Effect.logWarning(
|
|
301
|
-
`check run on ${run.repo}@${run.headSha} failed: ${GitHub.describe(e)}`,
|
|
302
|
-
),
|
|
266
|
+
Effect.logWarning(`check run on ${run.repo}@${run.headSha} failed: ${GitHub.describe(e)}`),
|
|
303
267
|
),
|
|
304
268
|
);
|
|
305
269
|
if (run.pr !== null) {
|
|
@@ -311,9 +275,7 @@ const publish = Effect.fn("publish")(function* (
|
|
|
311
275
|
.upsertComment(run.repo, run.pr, COMMENT_MARKER, body)
|
|
312
276
|
.pipe(
|
|
313
277
|
Effect.catch((e) =>
|
|
314
|
-
Effect.logWarning(
|
|
315
|
-
`comment on ${run.repo}#${run.pr} failed: ${GitHub.describe(e)}`,
|
|
316
|
-
),
|
|
278
|
+
Effect.logWarning(`comment on ${run.repo}#${run.pr} failed: ${GitHub.describe(e)}`),
|
|
317
279
|
),
|
|
318
280
|
);
|
|
319
281
|
}
|
|
@@ -353,11 +315,7 @@ const uploadTarball = Effect.fn("uploadTarball")(function* (
|
|
|
353
315
|
// The params schema already constrained this to 64 hex chars.
|
|
354
316
|
sha256: Result.getOrThrow(Hex.decode(sha256)),
|
|
355
317
|
})
|
|
356
|
-
.pipe(
|
|
357
|
-
Effect.mapError(
|
|
358
|
-
(e) => new BadRequest({ message: `upload rejected: ${String(e)}` }),
|
|
359
|
-
),
|
|
360
|
-
);
|
|
318
|
+
.pipe(Effect.mapError((e) => new BadRequest({ message: `upload rejected: ${String(e)}` })));
|
|
361
319
|
return { name, sha256, size: contentLength, uploaded: true };
|
|
362
320
|
});
|
|
363
321
|
|
|
@@ -380,9 +338,7 @@ export const parseInstallPath = (
|
|
|
380
338
|
// Malformed percent-encoding is a path nothing can be published under.
|
|
381
339
|
return undefined;
|
|
382
340
|
}
|
|
383
|
-
const tarball = path.match(
|
|
384
|
-
/^(@[^/]+\/[^/@]+|[^/@]+)\/-\/([a-f0-9]{64})\.tgz$/,
|
|
385
|
-
);
|
|
341
|
+
const tarball = path.match(/^(@[^/]+\/[^/@]+|[^/@]+)\/-\/([a-f0-9]{64})\.tgz$/);
|
|
386
342
|
if (tarball) {
|
|
387
343
|
return {
|
|
388
344
|
kind: "tarball",
|
|
@@ -403,8 +359,7 @@ export const parseInstallPath = (
|
|
|
403
359
|
const qualify = (name: string, scope: string | undefined) =>
|
|
404
360
|
scope !== undefined && !name.startsWith("@") ? `${scope}/${name}` : name;
|
|
405
361
|
|
|
406
|
-
const notFound = (message: string) =>
|
|
407
|
-
HttpServerResponse.json({ error: message }, { status: 404 });
|
|
362
|
+
const notFound = (message: string) => HttpServerResponse.json({ error: message }, { status: 404 });
|
|
408
363
|
|
|
409
364
|
/**
|
|
410
365
|
* Install URLs: `/<name>/<tag>` redirects to the immutable tarball URL,
|
|
@@ -417,16 +372,10 @@ const install = Effect.gen(function* () {
|
|
|
417
372
|
const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
|
|
418
373
|
|
|
419
374
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
420
|
-
return yield* HttpServerResponse.json(
|
|
421
|
-
{ error: "method not allowed" },
|
|
422
|
-
{ status: 405 },
|
|
423
|
-
);
|
|
375
|
+
return yield* HttpServerResponse.json({ error: "method not allowed" }, { status: 405 });
|
|
424
376
|
}
|
|
425
377
|
const host = request.headers.host ?? "localhost";
|
|
426
|
-
const target = parseInstallPath(
|
|
427
|
-
new URL(request.url, `https://${host}`).pathname,
|
|
428
|
-
aliases[host],
|
|
429
|
-
);
|
|
378
|
+
const target = parseInstallPath(new URL(request.url, `https://${host}`).pathname, aliases[host]);
|
|
430
379
|
if (target === undefined) {
|
|
431
380
|
return yield* notFound("not found");
|
|
432
381
|
}
|
|
@@ -490,9 +439,7 @@ export const handler = Effect.gen(function* () {
|
|
|
490
439
|
|
|
491
440
|
// Built once per isolate. The D1 client memoizes its connection on each
|
|
492
441
|
// event's scope, so its layer is safe to share.
|
|
493
|
-
const github = yield* GitHub.GitHubApp.pipe(
|
|
494
|
-
Effect.provide(GitHub.GitHubAppLive),
|
|
495
|
-
);
|
|
442
|
+
const github = yield* GitHub.GitHubApp.pipe(Effect.provide(GitHub.GitHubAppLive));
|
|
496
443
|
const services = Layer.mergeAll(
|
|
497
444
|
Cloudflare.R2.ReadWriteBucketBinding,
|
|
498
445
|
Cloudflare.KV.ReadWriteNamespaceBinding,
|
|
@@ -509,23 +456,20 @@ export const handler = Effect.gen(function* () {
|
|
|
509
456
|
(ref) => Effect.map(lookupRun(ref), (value) => ({ value, ttl: RUN_CACHE })),
|
|
510
457
|
);
|
|
511
458
|
|
|
512
|
-
yield* Cloudflare.Workers.cron(config.cron, () =>
|
|
513
|
-
sweep.pipe(Effect.provide(services)),
|
|
514
|
-
);
|
|
459
|
+
yield* Cloudflare.Workers.cron(config.cron, () => sweep.pipe(Effect.provide(services)));
|
|
515
460
|
|
|
516
461
|
const registry = HttpApiBuilder.group(PkgApi, "Registry", (handlers) =>
|
|
517
462
|
handlers
|
|
518
463
|
.handle("publish", ({ payload }) =>
|
|
519
|
-
Effect.flatMap(runs(payload.run), (run) =>
|
|
520
|
-
|
|
521
|
-
)
|
|
464
|
+
Effect.flatMap(runs(payload.run), (run) => publish(run, payload.manifest)).pipe(
|
|
465
|
+
Effect.catchTag(storageFailures, (e) => Effect.die(e)),
|
|
466
|
+
),
|
|
522
467
|
)
|
|
523
468
|
// Raw so the body streams into R2 instead of being buffered.
|
|
524
469
|
.handleRaw("uploadTarball", ({ query, params, request }) =>
|
|
525
|
-
Effect.andThen(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
).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
|
+
),
|
|
529
473
|
),
|
|
530
474
|
);
|
|
531
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$/);
|
package/src/Registry/Tags.ts
CHANGED
|
@@ -71,9 +71,7 @@ export const resolve = (pkg: string, tag: string) =>
|
|
|
71
71
|
Effect.map(get(pkg, tag), (row) => row?.sha256);
|
|
72
72
|
|
|
73
73
|
/** Rows tied to pull requests whose expiry falls before `before`. */
|
|
74
|
-
export const dueLinked = Effect.fn("Tags.dueLinked")(function* (
|
|
75
|
-
before: number,
|
|
76
|
-
) {
|
|
74
|
+
export const dueLinked = Effect.fn("Tags.dueLinked")(function* (before: number) {
|
|
77
75
|
const sql = yield* SqlClient.SqlClient;
|
|
78
76
|
return yield* decodeTags(
|
|
79
77
|
yield* sql`
|
|
@@ -94,9 +92,7 @@ export const setExpiry = Effect.fn("Tags.setExpiry")(function* (
|
|
|
94
92
|
});
|
|
95
93
|
|
|
96
94
|
/** Delete every expired row and return the tarballs those rows pointed at. */
|
|
97
|
-
export const deleteExpired = Effect.fn("Tags.deleteExpired")(function* (
|
|
98
|
-
now: number,
|
|
99
|
-
) {
|
|
95
|
+
export const deleteExpired = Effect.fn("Tags.deleteExpired")(function* (now: number) {
|
|
100
96
|
const sql = yield* SqlClient.SqlClient;
|
|
101
97
|
return yield* decodeTarballs(
|
|
102
98
|
yield* sql`
|
|
@@ -106,12 +102,8 @@ export const deleteExpired = Effect.fn("Tags.deleteExpired")(function* (
|
|
|
106
102
|
});
|
|
107
103
|
|
|
108
104
|
/** Every tarball some tag still points at, as `<package>/<sha256>`. */
|
|
109
|
-
export const referencedTarballs = Effect.fn("Tags.referencedTarballs")(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
return new Set(rows.map((row) => `${row.package}/${row.sha256}`));
|
|
116
|
-
},
|
|
117
|
-
);
|
|
105
|
+
export const referencedTarballs = Effect.fn("Tags.referencedTarballs")(function* () {
|
|
106
|
+
const sql = yield* SqlClient.SqlClient;
|
|
107
|
+
const rows = yield* decodeTarballs(yield* sql`SELECT DISTINCT package, sha256 FROM tags`);
|
|
108
|
+
return new Set(rows.map((row) => `${row.package}/${row.sha256}`));
|
|
109
|
+
});
|
package/src/Registry.ts
CHANGED
|
@@ -20,14 +20,7 @@ import {
|
|
|
20
20
|
} from "./Registry/Bindings.ts";
|
|
21
21
|
import { handler } from "./Registry/Handler.ts";
|
|
22
22
|
|
|
23
|
-
export {
|
|
24
|
-
Bucket,
|
|
25
|
-
Cache,
|
|
26
|
-
Database,
|
|
27
|
-
Policy,
|
|
28
|
-
RegistryConfig,
|
|
29
|
-
Settings,
|
|
30
|
-
} from "./Registry/Bindings.ts";
|
|
23
|
+
export { Bucket, Cache, Database, Policy, RegistryConfig, Settings } from "./Registry/Bindings.ts";
|
|
31
24
|
|
|
32
25
|
export interface RegistryOptions {
|
|
33
26
|
/**
|
|
@@ -99,9 +92,9 @@ const RegistryConfigFromEnv = Layer.effect(
|
|
|
99
92
|
RegistryConfig,
|
|
100
93
|
Effect.gen(function* () {
|
|
101
94
|
const env = yield* Cloudflare.Workers.WorkerEnvironment;
|
|
102
|
-
const settings = yield* Schema.decodeUnknownEffect(Settings)(
|
|
103
|
-
|
|
104
|
-
)
|
|
95
|
+
const settings = yield* Schema.decodeUnknownEffect(Settings)(env[SETTINGS_ENV]).pipe(
|
|
96
|
+
Effect.orDie,
|
|
97
|
+
);
|
|
105
98
|
return {
|
|
106
99
|
...settings,
|
|
107
100
|
worker: {},
|
|
@@ -165,10 +158,7 @@ export default RegistryLive.pipe(Layer.provide(RegistryConfigFromEnv));
|
|
|
165
158
|
* @product Workers
|
|
166
159
|
* @category Workers & Compute
|
|
167
160
|
*/
|
|
168
|
-
export const PkgRegistry = <const Id extends string>(
|
|
169
|
-
id: Id,
|
|
170
|
-
options: RegistryOptions,
|
|
171
|
-
) =>
|
|
161
|
+
export const PkgRegistry = <const Id extends string>(id: Id, options: RegistryOptions) =>
|
|
172
162
|
Registry.pipe(
|
|
173
163
|
Effect.provide(
|
|
174
164
|
RegistryLive.pipe(
|