@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.
Files changed (57) hide show
  1. package/README.md +17 -5
  2. package/dist/Manifest.d.ts.map +1 -1
  3. package/dist/Manifest.js.map +1 -1
  4. package/dist/Protocol.d.ts +3 -3
  5. package/dist/Protocol.d.ts.map +1 -1
  6. package/dist/Protocol.js +6 -15
  7. package/dist/Protocol.js.map +1 -1
  8. package/dist/Registry/Bindings.d.ts.map +1 -1
  9. package/dist/Registry/Bindings.js.map +1 -1
  10. package/dist/Registry/GitHub.d.ts +7 -7
  11. package/dist/Registry/GitHub.d.ts.map +1 -1
  12. package/dist/Registry/GitHub.js +20 -45
  13. package/dist/Registry/GitHub.js.map +1 -1
  14. package/dist/Registry/Handler.d.ts +5 -5
  15. package/dist/Registry/Handler.d.ts.map +1 -1
  16. package/dist/Registry/Handler.js +26 -27
  17. package/dist/Registry/Handler.js.map +1 -1
  18. package/dist/Registry/KVCache.d.ts.map +1 -1
  19. package/dist/Registry/KVCache.js +1 -1
  20. package/dist/Registry/KVCache.js.map +1 -1
  21. package/dist/Registry/Sweep.d.ts +1 -1
  22. package/dist/Registry/Sweep.d.ts.map +1 -1
  23. package/dist/Registry/Sweep.js +3 -8
  24. package/dist/Registry/Sweep.js.map +1 -1
  25. package/dist/Registry/Tags.d.ts +8 -8
  26. package/dist/Registry/Tags.d.ts.map +1 -1
  27. package/dist/Registry/Tags.js +1 -1
  28. package/dist/Registry/Tags.js.map +1 -1
  29. package/dist/Registry.d.ts +2 -2
  30. package/dist/Registry.d.ts.map +1 -1
  31. package/dist/Registry.js +1 -1
  32. package/dist/Registry.js.map +1 -1
  33. package/dist/cli/bin.d.ts +8 -4
  34. package/dist/cli/bin.d.ts.map +1 -1
  35. package/dist/cli/bin.js +22 -10
  36. package/dist/cli/bin.js.map +1 -1
  37. package/dist/cli/pack.d.ts +11 -4
  38. package/dist/cli/pack.d.ts.map +1 -1
  39. package/dist/cli/pack.js +91 -30
  40. package/dist/cli/pack.js.map +1 -1
  41. package/dist/cli/publish.d.ts +1 -1
  42. package/dist/cli/publish.d.ts.map +1 -1
  43. package/dist/cli/publish.js +6 -2
  44. package/dist/cli/publish.js.map +1 -1
  45. package/package.json +23 -23
  46. package/src/Manifest.ts +1 -3
  47. package/src/Protocol.ts +15 -31
  48. package/src/Registry/Bindings.ts +1 -4
  49. package/src/Registry/GitHub.ts +34 -86
  50. package/src/Registry/Handler.ts +46 -90
  51. package/src/Registry/KVCache.ts +6 -17
  52. package/src/Registry/Sweep.ts +13 -26
  53. package/src/Registry/Tags.ts +8 -16
  54. package/src/Registry.ts +5 -15
  55. package/src/cli/bin.ts +35 -20
  56. package/src/cli/pack.ts +142 -82
  57. package/src/cli/publish.ts +16 -23
@@ -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 * as Encoding from "effect/Encoding";
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 = Encoding.encodeBase64Url(
54
- JSON.stringify({ alg: "RS256", typ: "JWT" }),
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}.${Encoding.encodeBase64Url(new Uint8Array(signature))}`;
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: string,
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
- * Pull requests against `repo` whose head is `sha` in `headRepo`, the
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
- Repos.listPullRequestsAssociatedWithCommit({
219
+ Pulls.list({
253
220
  ...split(repo),
254
- commit_sha: sha,
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
- Effect.Success<typeof make>
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);
@@ -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 * as Encoding from "effect/Encoding";
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. A fork's run gets
85
- * only `pr:N`: commit tags are shared by every publisher, and a fork can
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 ? [] : [`pr:${run.pr}`];
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
- isFork(run) ? `pr:${run.pr}` : run.headSha.slice(0, SHORT);
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
- manifestText,
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(Encoding.decodeHex(sha256)),
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
- publish(run, payload.manifest),
509
- ).pipe(Effect.catchTag(storageFailures, (e) => Effect.die(e))),
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
- runs(query),
515
- uploadTarball(request, params.name, params.sha256),
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
 
@@ -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
- kv.put(id, text, { expirationTtl: seconds(fresh.ttl) }),
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;
@@ -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
- `pull request ${ref} lookup failed: ${GitHub.describe(e)}`,
48
- ).pipe(Effect.as([ref, undefined] as const)),
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
- undefined as string | undefined,
90
- (cursor) =>
91
- r2
92
- .list({ cursor, limit: 500 })
93
- .pipe(
94
- Effect.map(
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$/);