@alchemy.run/pkg 2.0.0-beta.80 → 2.0.0-beta.82
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 +10 -37
- 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 +25 -33
- 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 +1 -1
- package/dist/Registry/Tags.js.map +1 -1
- package/dist/Registry.d.ts +23 -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 +6 -10
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/pack.d.ts +11 -0
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +97 -24
- 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 +26 -25
- package/src/Manifest.ts +1 -3
- package/src/Protocol.ts +12 -28
- package/src/Registry/Bindings.ts +1 -4
- package/src/Registry/GitHub.ts +25 -84
- package/src/Registry/Handler.ts +44 -100
- 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 +9 -19
- package/src/cli/pack.ts +126 -74
- package/src/cli/publish.ts +11 -23
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,12 +91,10 @@ 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
|
+
Upstream.make({ message: GitHub.describe(e) });
|
|
107
98
|
|
|
108
99
|
/**
|
|
109
100
|
* Storage failures are not part of the protocol: they are defects, which
|
|
@@ -121,19 +112,17 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
121
112
|
const github = yield* GitHub.GitHubApp;
|
|
122
113
|
|
|
123
114
|
if (!policy.repos.includes(ref.repo)) {
|
|
124
|
-
return yield*
|
|
115
|
+
return yield* Forbidden.make({ 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)
|
|
132
121
|
) {
|
|
133
|
-
return yield*
|
|
122
|
+
return yield* RunNotInProgress.make({ message: "run is not in progress" });
|
|
134
123
|
}
|
|
135
124
|
if (data.event !== "push" && data.event !== "pull_request") {
|
|
136
|
-
return yield*
|
|
125
|
+
return yield* BadRequest.make({
|
|
137
126
|
message: `unsupported event ${data.event}`,
|
|
138
127
|
});
|
|
139
128
|
}
|
|
@@ -141,7 +130,7 @@ const lookupRun = Effect.fn("lookupRun")(function* (ref: RunRef) {
|
|
|
141
130
|
let pr: number | null = null;
|
|
142
131
|
if (data.event === "pull_request") {
|
|
143
132
|
if (data.head_branch === null) {
|
|
144
|
-
return yield*
|
|
133
|
+
return yield* BadRequest.make({
|
|
145
134
|
message: `pull request run ${ref.runId} has no head branch`,
|
|
146
135
|
});
|
|
147
136
|
}
|
|
@@ -151,22 +140,17 @@ 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) {
|
|
163
|
-
return yield*
|
|
147
|
+
return yield* BadRequest.make({
|
|
164
148
|
message: `no open pull request from ${headRepo} has head ${data.head_sha}`,
|
|
165
149
|
});
|
|
166
150
|
}
|
|
167
151
|
pr = first.number;
|
|
168
152
|
} else if (headRepo !== ref.repo) {
|
|
169
|
-
return yield*
|
|
153
|
+
return yield* BadRequest.make({
|
|
170
154
|
message: `push run ${ref.runId} is for ${headRepo}, not ${ref.repo}`,
|
|
171
155
|
});
|
|
172
156
|
}
|
|
@@ -185,56 +169,44 @@ 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
|
|
195
176
|
.listRunArtifacts(run.repo, run.runId, expected)
|
|
196
177
|
.pipe(Effect.mapError(upstream));
|
|
197
178
|
if (!artifacts.some((a) => a.name === expected && !a.expired)) {
|
|
198
|
-
return yield*
|
|
179
|
+
return yield* Forbidden.make({
|
|
199
180
|
message: `run ${run.runId} has not vouched for this manifest (no artifact ${expected})`,
|
|
200
181
|
});
|
|
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) => BadRequest.make({ 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`
|
|
223
197
|
// job that packs the synthetic merge commit would otherwise publish bytes
|
|
224
198
|
// built from a commit that exists nowhere under the commit's name.
|
|
225
199
|
if (manifest.head !== run.headSha) {
|
|
226
|
-
return yield*
|
|
200
|
+
return yield* BadRequest.make({
|
|
227
201
|
message: `manifest was packed at ${manifest.head}, but the run's head is ${run.headSha}`,
|
|
228
202
|
});
|
|
229
203
|
}
|
|
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
|
-
return yield*
|
|
209
|
+
return yield* PackageTooLarge.make({
|
|
238
210
|
message: `${tooLarge.name} exceeds ${maxSize} bytes`,
|
|
239
211
|
});
|
|
240
212
|
}
|
|
@@ -242,17 +214,11 @@ 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
|
-
return yield*
|
|
221
|
+
return yield* MissingTarballs.make({ missing });
|
|
256
222
|
}
|
|
257
223
|
|
|
258
224
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -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
|
}
|
|
@@ -334,11 +296,11 @@ const uploadTarball = Effect.fn("uploadTarball")(function* (
|
|
|
334
296
|
|
|
335
297
|
const contentLength = Number(request.headers["content-length"] ?? 0);
|
|
336
298
|
if (!Number.isSafeInteger(contentLength) || contentLength <= 0) {
|
|
337
|
-
return yield*
|
|
299
|
+
return yield* BadRequest.make({ message: "Content-Length is required" });
|
|
338
300
|
}
|
|
339
301
|
const maxSize = policy.maxPackageSize;
|
|
340
302
|
if (maxSize !== undefined && BigInt(contentLength) > maxSize) {
|
|
341
|
-
return yield*
|
|
303
|
+
return yield* PackageTooLarge.make({
|
|
342
304
|
message: `tarball exceeds ${maxSize} bytes`,
|
|
343
305
|
});
|
|
344
306
|
}
|
|
@@ -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) => BadRequest.make({ 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
|
@@ -9,7 +9,7 @@ import * as SqlClient from "effect/sql/SqlClient";
|
|
|
9
9
|
* D1 binding.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const LinkedPrs = Schema.
|
|
12
|
+
const LinkedPrs = Schema.String.pipe(Schema.Array, Schema.fromJsonString);
|
|
13
13
|
|
|
14
14
|
export const TagRow = Schema.Struct({
|
|
15
15
|
package: Schema.String,
|
|
@@ -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(
|
package/src/cli/bin.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { PlatformServices, runMain } from "alchemy/Util/PlatformServices";
|
|
3
|
+
import { CliConfig, CliError, Command, Flag, GlobalFlag } from "effect/cli";
|
|
3
4
|
import * as Config from "effect/Config";
|
|
4
5
|
import * as Effect from "effect/Effect";
|
|
5
|
-
import * as Option from "effect/Option";
|
|
6
|
-
import * as Layer from "effect/Layer";
|
|
7
|
-
import { CliConfig, CliError, Command, Flag, GlobalFlag } from "effect/cli";
|
|
8
6
|
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
9
|
import pkg from "../../package.json" with { type: "json" };
|
|
10
10
|
import { Group, pack } from "./pack.ts";
|
|
11
11
|
import { publish } from "./publish.ts";
|
|
@@ -38,16 +38,12 @@ export const packCommand = Command.make(
|
|
|
38
38
|
registry: registryFlag,
|
|
39
39
|
out: outFlag,
|
|
40
40
|
since: Flag.String("since").pipe(
|
|
41
|
-
Flag.withDescription(
|
|
42
|
-
"Compare HEAD with this Git ref; defaults to the PR base in Actions",
|
|
43
|
-
),
|
|
41
|
+
Flag.withDescription("Compare HEAD with this Git ref; defaults to the PR base in Actions"),
|
|
44
42
|
Flag.optional,
|
|
45
43
|
),
|
|
46
44
|
all: Flag.Boolean("all").pipe(
|
|
47
45
|
Flag.withDefault(false),
|
|
48
|
-
Flag.withDescription(
|
|
49
|
-
"Pack every configured package regardless of changes",
|
|
50
|
-
),
|
|
46
|
+
Flag.withDescription("Pack every configured package regardless of changes"),
|
|
51
47
|
),
|
|
52
48
|
rebuildAllPaths: Flag.String("rebuild-all-path").pipe(
|
|
53
49
|
Flag.withDescription(
|
|
@@ -98,17 +94,13 @@ export const publishCommand = Command.make(
|
|
|
98
94
|
Command.withDescription(
|
|
99
95
|
"Publish a pkg pack directory from the current GitHub Actions job, after its manifest artifact has been uploaded",
|
|
100
96
|
),
|
|
101
|
-
Command.withExamples([
|
|
102
|
-
{ command: "pkg publish --registry https://pkg.alchemy.run" },
|
|
103
|
-
]),
|
|
97
|
+
Command.withExamples([{ command: "pkg publish --registry https://pkg.alchemy.run" }]),
|
|
104
98
|
);
|
|
105
99
|
|
|
106
100
|
export const root = Command.make("pkg", {}, () =>
|
|
107
|
-
Effect.fail(
|
|
101
|
+
Effect.fail(CliError.ShowHelp.make({ commandPath: ["pkg"], errors: [] })),
|
|
108
102
|
).pipe(
|
|
109
|
-
Command.withDescription(
|
|
110
|
-
"Pack and publish preview packages for pull requests.",
|
|
111
|
-
),
|
|
103
|
+
Command.withDescription("Pack and publish preview packages for pull requests."),
|
|
112
104
|
Command.withSubcommands([packCommand, publishCommand]),
|
|
113
105
|
);
|
|
114
106
|
|
|
@@ -117,9 +109,7 @@ Command.run(root, { version: pkg.version }).pipe(
|
|
|
117
109
|
Layer.mergeAll(
|
|
118
110
|
PlatformServices,
|
|
119
111
|
FetchHttpClient.layer,
|
|
120
|
-
CliConfig.layer({
|
|
121
|
-
builtIns: [GlobalFlag.Help, GlobalFlag.Version],
|
|
122
|
-
}),
|
|
112
|
+
CliConfig.layer({ builtIns: [GlobalFlag.Help, GlobalFlag.Version] }),
|
|
123
113
|
),
|
|
124
114
|
),
|
|
125
115
|
runMain,
|