@alchemy.run/pkg 0.0.0-placeholder → 2.0.0-beta.78
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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +117 -0
- package/dist/Manifest.d.ts +107 -0
- package/dist/Manifest.d.ts.map +1 -0
- package/dist/Manifest.js +58 -0
- package/dist/Manifest.js.map +1 -0
- package/dist/Protocol.d.ts +186 -0
- package/dist/Protocol.d.ts.map +1 -0
- package/dist/Protocol.js +115 -0
- package/dist/Protocol.js.map +1 -0
- package/dist/Registry/Bindings.d.ts +134 -0
- package/dist/Registry/Bindings.d.ts.map +1 -0
- package/dist/Registry/Bindings.js +92 -0
- package/dist/Registry/Bindings.js.map +1 -0
- package/dist/Registry/GitHub.d.ts +96 -0
- package/dist/Registry/GitHub.d.ts.map +1 -0
- package/dist/Registry/GitHub.js +226 -0
- package/dist/Registry/GitHub.js.map +1 -0
- package/dist/Registry/Handler.d.ts +53 -0
- package/dist/Registry/Handler.d.ts.map +1 -0
- package/dist/Registry/Handler.js +375 -0
- package/dist/Registry/Handler.js.map +1 -0
- package/dist/Registry/KVCache.d.ts +24 -0
- package/dist/Registry/KVCache.d.ts.map +1 -0
- package/dist/Registry/KVCache.js +42 -0
- package/dist/Registry/KVCache.js.map +1 -0
- package/dist/Registry/Sweep.d.ts +12 -0
- package/dist/Registry/Sweep.d.ts.map +1 -0
- package/dist/Registry/Sweep.js +69 -0
- package/dist/Registry/Sweep.js.map +1 -0
- package/dist/Registry/Tags.d.ts +49 -0
- package/dist/Registry/Tags.d.ts.map +1 -0
- package/dist/Registry/Tags.js +80 -0
- package/dist/Registry/Tags.js.map +1 -0
- package/dist/Registry.d.ts +378 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +119 -0
- package/dist/Registry.js.map +1 -0
- package/dist/cli/bin.d.ts +18 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +33 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/pack.d.ts +169 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +447 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/cli/publish.d.ts +33 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/publish.js +76 -0
- package/dist/cli/publish.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/migrations/0001_init.sql +19 -0
- package/package.json +89 -1
- package/src/Manifest.ts +76 -0
- package/src/Protocol.ts +163 -0
- package/src/Registry/Bindings.ts +134 -0
- package/src/Registry/GitHub.ts +365 -0
- package/src/Registry/Handler.ts +549 -0
- package/src/Registry/KVCache.ts +68 -0
- package/src/Registry/Sweep.ts +117 -0
- package/src/Registry/Tags.ts +117 -0
- package/src/Registry.ts +197 -0
- package/src/cli/bin.ts +101 -0
- package/src/cli/pack.ts +603 -0
- package/src/cli/publish.ts +129 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as Cloudflare from "alchemy/Cloudflare";
|
|
2
|
+
import * as Clock from "effect/Clock";
|
|
3
|
+
import * as Duration from "effect/Duration";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
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";
|
|
14
|
+
import * as GitHub from "./GitHub.ts";
|
|
15
|
+
import * as Tags from "./Tags.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Expiry sweep. Tags tied to pull requests are extended while any of those
|
|
19
|
+
* pull requests is open and pinned to the latest close time plus TTL once
|
|
20
|
+
* they all close. Expired tags go away, and tarballs nothing points at are
|
|
21
|
+
* deleted from R2.
|
|
22
|
+
*/
|
|
23
|
+
export const sweep = Effect.gen(function* () {
|
|
24
|
+
const { policy } = yield* RegistryConfig;
|
|
25
|
+
const r2 = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
|
|
26
|
+
const github = yield* GitHub.GitHubApp;
|
|
27
|
+
|
|
28
|
+
const now = yield* Clock.currentTimeMillis;
|
|
29
|
+
const ttl = Duration.toMillis(policy.ttl);
|
|
30
|
+
const due = yield* Tags.dueLinked(now + Duration.toMillis(SWEEP_LOOKAHEAD));
|
|
31
|
+
|
|
32
|
+
// One lookup per pull request across every due row. `undefined` means the
|
|
33
|
+
// pull request is still open; a failed lookup is recorded as such.
|
|
34
|
+
const lookups = yield* Effect.forEach(
|
|
35
|
+
new Set(due.flatMap((row) => row.linked_prs)),
|
|
36
|
+
(ref) => {
|
|
37
|
+
const [repo, number] = ref.split("#");
|
|
38
|
+
return github.getPullRequest(repo!, Number(number)).pipe(
|
|
39
|
+
Effect.map((pr) =>
|
|
40
|
+
pr.state === "open"
|
|
41
|
+
? undefined
|
|
42
|
+
: Date.parse(pr.merged_at ?? pr.closed_at ?? "") || now,
|
|
43
|
+
),
|
|
44
|
+
Effect.map((closedAt) => [ref, { closedAt }] as const),
|
|
45
|
+
Effect.catch((e) =>
|
|
46
|
+
Effect.logWarning(
|
|
47
|
+
`pull request ${ref} lookup failed: ${GitHub.describe(e)}`,
|
|
48
|
+
).pipe(Effect.as([ref, undefined] as const)),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
{ concurrency: 4 },
|
|
53
|
+
);
|
|
54
|
+
const results = new Map(lookups);
|
|
55
|
+
const extended = yield* Effect.forEach(due, (row) => {
|
|
56
|
+
const known = row.linked_prs.flatMap((ref) => {
|
|
57
|
+
const result = results.get(ref);
|
|
58
|
+
return result === undefined ? [] : [result.closedAt];
|
|
59
|
+
});
|
|
60
|
+
// Open pull requests dominate; otherwise the latest close wins. A row
|
|
61
|
+
// whose pull requests could not all be checked is kept past the next
|
|
62
|
+
// sweep rather than left to expire: it is re-checked, never deleted,
|
|
63
|
+
// on GitHub's bad day.
|
|
64
|
+
const closes = known.filter((closedAt) => closedAt !== undefined);
|
|
65
|
+
const expiresAt =
|
|
66
|
+
closes.length < known.length
|
|
67
|
+
? now + ttl
|
|
68
|
+
: known.length < row.linked_prs.length
|
|
69
|
+
? Math.max(row.expires_at, now + Duration.toMillis(SWEEP_LOOKAHEAD))
|
|
70
|
+
: Math.max(...closes) + ttl;
|
|
71
|
+
return expiresAt === row.expires_at
|
|
72
|
+
? Effect.succeed(false)
|
|
73
|
+
: Tags.setExpiry(row.package, row.tag, expiresAt).pipe(Effect.as(true));
|
|
74
|
+
}).pipe(Effect.map((changed) => changed.filter(Boolean).length));
|
|
75
|
+
|
|
76
|
+
const removed = yield* Tags.deleteExpired(now);
|
|
77
|
+
const referenced = yield* Tags.referencedTarballs();
|
|
78
|
+
const unreferenced = removed.filter(
|
|
79
|
+
({ package: pkg, sha256 }) => !referenced.has(`${pkg}/${sha256}`),
|
|
80
|
+
);
|
|
81
|
+
yield* Effect.forEach(
|
|
82
|
+
unreferenced,
|
|
83
|
+
({ package: pkg, sha256 }) => r2.delete(tarballKey(pkg, sha256)),
|
|
84
|
+
{ discard: true },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 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
|
+
),
|
|
101
|
+
),
|
|
102
|
+
).pipe(
|
|
103
|
+
Stream.filter((object) => {
|
|
104
|
+
const match = object.key.match(/^(.+)\/([a-f0-9]{64})\.tgz$/);
|
|
105
|
+
return (
|
|
106
|
+
match !== null &&
|
|
107
|
+
!referenced.has(`${decodeURIComponent(match[1]!)}/${match[2]}`) &&
|
|
108
|
+
object.uploaded.getTime() < now - Duration.toMillis(ORPHAN_GRACE)
|
|
109
|
+
);
|
|
110
|
+
}),
|
|
111
|
+
Stream.mapEffect((object) => r2.delete(object.key)),
|
|
112
|
+
Stream.runCount,
|
|
113
|
+
);
|
|
114
|
+
yield* Effect.logInfo(
|
|
115
|
+
`sweep: ${due.length} due, ${extended} extended, ${removed.length} tags expired, ${unreferenced.length + orphans} tarballs deleted`,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `tags` table: one row per `(package, tag)` pointing at a tarball hash.
|
|
7
|
+
* A tarball lives in R2 for as long as any row points at it. Every query
|
|
8
|
+
* runs against the ambient `SqlClient`, which the Worker provides from its
|
|
9
|
+
* D1 binding.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const LinkedPrs = Schema.fromJsonString(Schema.Array(Schema.String));
|
|
13
|
+
|
|
14
|
+
export const TagRow = Schema.Struct({
|
|
15
|
+
package: Schema.String,
|
|
16
|
+
tag: Schema.String,
|
|
17
|
+
sha256: Schema.String,
|
|
18
|
+
expires_at: Schema.Number,
|
|
19
|
+
/** Pull requests whose runs produced this tag, as `owner/repo#N`. */
|
|
20
|
+
linked_prs: LinkedPrs,
|
|
21
|
+
});
|
|
22
|
+
export type TagRow = typeof TagRow.Type;
|
|
23
|
+
|
|
24
|
+
const TarballRow = Schema.Struct({
|
|
25
|
+
package: Schema.String,
|
|
26
|
+
sha256: Schema.String,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const decodeTags = Schema.decodeUnknownEffect(Schema.Array(TagRow));
|
|
30
|
+
const decodeTarballs = Schema.decodeUnknownEffect(Schema.Array(TarballRow));
|
|
31
|
+
|
|
32
|
+
export const get = Effect.fn("Tags.get")(function* (pkg: string, tag: string) {
|
|
33
|
+
const sql = yield* SqlClient.SqlClient;
|
|
34
|
+
const rows = yield* decodeTags(
|
|
35
|
+
yield* sql`SELECT * FROM tags WHERE package = ${pkg} AND tag = ${tag}`,
|
|
36
|
+
);
|
|
37
|
+
return rows[0];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Point `tag` at `sha256`, keeping the row alive for at least `expiresAt`
|
|
42
|
+
* and remembering every pull request that produced it. One statement, so
|
|
43
|
+
* concurrent publications of a shared tag merge rather than overwrite.
|
|
44
|
+
*/
|
|
45
|
+
export const upsert = Effect.fn("Tags.upsert")(function* (input: {
|
|
46
|
+
readonly package: string;
|
|
47
|
+
readonly tag: string;
|
|
48
|
+
readonly sha256: string;
|
|
49
|
+
readonly expiresAt: number;
|
|
50
|
+
readonly prs: ReadonlyArray<string>;
|
|
51
|
+
}) {
|
|
52
|
+
const sql = yield* SqlClient.SqlClient;
|
|
53
|
+
const prs = yield* Schema.encodeEffect(LinkedPrs)(input.prs);
|
|
54
|
+
yield* sql`
|
|
55
|
+
INSERT INTO tags (package, tag, sha256, expires_at, linked_prs)
|
|
56
|
+
VALUES (${input.package}, ${input.tag}, ${input.sha256}, ${input.expiresAt}, ${prs})
|
|
57
|
+
ON CONFLICT (package, tag) DO UPDATE SET
|
|
58
|
+
sha256 = excluded.sha256,
|
|
59
|
+
expires_at = max(tags.expires_at, excluded.expires_at),
|
|
60
|
+
linked_prs = (
|
|
61
|
+
SELECT json_group_array(value) FROM (
|
|
62
|
+
SELECT value FROM json_each(tags.linked_prs)
|
|
63
|
+
UNION
|
|
64
|
+
SELECT value FROM json_each(excluded.linked_prs)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
`;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const resolve = (pkg: string, tag: string) =>
|
|
71
|
+
Effect.map(get(pkg, tag), (row) => row?.sha256);
|
|
72
|
+
|
|
73
|
+
/** Rows tied to pull requests whose expiry falls before `before`. */
|
|
74
|
+
export const dueLinked = Effect.fn("Tags.dueLinked")(function* (
|
|
75
|
+
before: number,
|
|
76
|
+
) {
|
|
77
|
+
const sql = yield* SqlClient.SqlClient;
|
|
78
|
+
return yield* decodeTags(
|
|
79
|
+
yield* sql`
|
|
80
|
+
SELECT * FROM tags WHERE linked_prs != '[]' AND expires_at < ${before}
|
|
81
|
+
`,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const setExpiry = Effect.fn("Tags.setExpiry")(function* (
|
|
86
|
+
pkg: string,
|
|
87
|
+
tag: string,
|
|
88
|
+
expiresAt: number,
|
|
89
|
+
) {
|
|
90
|
+
const sql = yield* SqlClient.SqlClient;
|
|
91
|
+
yield* sql`
|
|
92
|
+
UPDATE tags SET expires_at = ${expiresAt} WHERE package = ${pkg} AND tag = ${tag}
|
|
93
|
+
`;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** 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
|
+
) {
|
|
100
|
+
const sql = yield* SqlClient.SqlClient;
|
|
101
|
+
return yield* decodeTarballs(
|
|
102
|
+
yield* sql`
|
|
103
|
+
DELETE FROM tags WHERE expires_at < ${now} RETURNING package, sha256
|
|
104
|
+
`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Every tarball some tag still points at, as `<package>/<sha256>`. */
|
|
109
|
+
export const referencedTarballs = Effect.fn("Tags.referencedTarballs")(
|
|
110
|
+
function* () {
|
|
111
|
+
const sql = yield* SqlClient.SqlClient;
|
|
112
|
+
const rows = yield* decodeTarballs(
|
|
113
|
+
yield* sql`SELECT DISTINCT package, sha256 FROM tags`,
|
|
114
|
+
);
|
|
115
|
+
return new Set(rows.map((row) => `${row.package}/${row.sha256}`));
|
|
116
|
+
},
|
|
117
|
+
);
|
package/src/Registry.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as Cloudflare from "alchemy/Cloudflare";
|
|
2
|
+
import * as Namespace from "alchemy/Namespace";
|
|
3
|
+
import type * as ByteSize from "effect/ByteSize";
|
|
4
|
+
import * as Config from "effect/Config";
|
|
5
|
+
import type * as Duration from "effect/Duration";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import {
|
|
10
|
+
APP_ID_ENV,
|
|
11
|
+
DEFAULT_API_URL,
|
|
12
|
+
DEFAULT_CRON,
|
|
13
|
+
DEFAULT_TTL,
|
|
14
|
+
PRIVATE_KEY_ENV,
|
|
15
|
+
RegistryConfig,
|
|
16
|
+
SETTINGS_ENV,
|
|
17
|
+
Settings,
|
|
18
|
+
type GitHubCredentials,
|
|
19
|
+
type WorkerOverrides,
|
|
20
|
+
} from "./Registry/Bindings.ts";
|
|
21
|
+
import { handler } from "./Registry/Handler.ts";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
Bucket,
|
|
25
|
+
Cache,
|
|
26
|
+
Database,
|
|
27
|
+
Policy,
|
|
28
|
+
RegistryConfig,
|
|
29
|
+
Settings,
|
|
30
|
+
} from "./Registry/Bindings.ts";
|
|
31
|
+
|
|
32
|
+
export interface RegistryOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Worker props forwarded verbatim: `domain`, `name`, `compatibility`,
|
|
35
|
+
* `observability`, `access`, and so on. `main`, `env`, and `crons` are
|
|
36
|
+
* owned by the registry.
|
|
37
|
+
*/
|
|
38
|
+
readonly worker?: WorkerOverrides;
|
|
39
|
+
/** GitHub App credentials, read at deploy time and bound to the Worker. */
|
|
40
|
+
readonly github: GitHubCredentials & {
|
|
41
|
+
/** @default "https://api.github.com" */
|
|
42
|
+
readonly apiUrl?: string;
|
|
43
|
+
};
|
|
44
|
+
readonly policy: {
|
|
45
|
+
/** Repositories allowed to publish, as `owner/name`. */
|
|
46
|
+
readonly repos: ReadonlyArray<string>;
|
|
47
|
+
/** @default Duration.weeks(1) */
|
|
48
|
+
readonly ttl?: Duration.Duration;
|
|
49
|
+
/** Upper bound on a single tarball. Absent means unlimited. */
|
|
50
|
+
readonly maxPackageSize?: ByteSize.ByteSize;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Hostname to package scope. Requests on an aliased host resolve an
|
|
54
|
+
* unscoped name under that scope, so `pkg.distilled.cloud/core/<sha>`
|
|
55
|
+
* serves `@distilled.cloud/core`.
|
|
56
|
+
*/
|
|
57
|
+
readonly aliases?: Readonly<Record<string, string>>;
|
|
58
|
+
/**
|
|
59
|
+
* Cron expression for the expiry sweep.
|
|
60
|
+
* @default "0 * * * *"
|
|
61
|
+
*/
|
|
62
|
+
readonly cron?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The registry Worker. Its props are an Effect so the same declaration
|
|
67
|
+
* serves both sides of the bundle boundary: at plan time
|
|
68
|
+
* {@link RegistryConfig} is the caller's options, inside the isolate it is
|
|
69
|
+
* rebuilt from the Worker's bindings.
|
|
70
|
+
*/
|
|
71
|
+
export class Registry extends Cloudflare.Worker<Registry, {}>()("Worker") {}
|
|
72
|
+
|
|
73
|
+
const RegistryLive = Registry.make(
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const config = yield* RegistryConfig;
|
|
76
|
+
return {
|
|
77
|
+
...config.worker,
|
|
78
|
+
main: import.meta.url,
|
|
79
|
+
env: {
|
|
80
|
+
[SETTINGS_ENV]: yield* Schema.encodeEffect(Settings)({
|
|
81
|
+
policy: config.policy,
|
|
82
|
+
aliases: config.aliases,
|
|
83
|
+
cron: config.cron,
|
|
84
|
+
github: { apiUrl: config.github.apiUrl },
|
|
85
|
+
}).pipe(Effect.orDie),
|
|
86
|
+
[APP_ID_ENV]: config.github.appId,
|
|
87
|
+
[PRIVATE_KEY_ENV]: config.github.privateKey,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
handler,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Runtime side of {@link RegistryConfig}: decode the settings binding and
|
|
96
|
+
* point the credentials at the bindings `RegistryLive` declared.
|
|
97
|
+
*/
|
|
98
|
+
const RegistryConfigFromEnv = Layer.effect(
|
|
99
|
+
RegistryConfig,
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const env = yield* Cloudflare.Workers.WorkerEnvironment;
|
|
102
|
+
const settings = yield* Schema.decodeUnknownEffect(Settings)(
|
|
103
|
+
env[SETTINGS_ENV],
|
|
104
|
+
).pipe(Effect.orDie);
|
|
105
|
+
return {
|
|
106
|
+
...settings,
|
|
107
|
+
worker: {},
|
|
108
|
+
github: {
|
|
109
|
+
...settings.github,
|
|
110
|
+
appId: Config.String(APP_ID_ENV),
|
|
111
|
+
privateKey: Config.Redacted(PRIVATE_KEY_ENV),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The bundle entry. `RegistryLive` names this file as `main`, so the
|
|
119
|
+
* generated Worker entry imports this default export and builds it once per
|
|
120
|
+
* isolate. User code never imports it; use {@link PkgRegistry}.
|
|
121
|
+
*/
|
|
122
|
+
export default RegistryLive.pipe(Layer.provide(RegistryConfigFromEnv));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A preview package registry: a Worker serving install URLs and the publish
|
|
126
|
+
* API, an R2 bucket of content-addressed tarballs, a D1 index of tags, and a
|
|
127
|
+
* KV namespace caching what the Worker learns from GitHub. They are declared
|
|
128
|
+
* under `id` as `<id>/Worker`, `<id>/Bucket`, `<id>/Database`, and
|
|
129
|
+
* `<id>/Cache`.
|
|
130
|
+
*
|
|
131
|
+
* ### Deploying a registry
|
|
132
|
+
* **Example:** Stack
|
|
133
|
+
* ```typescript
|
|
134
|
+
* // stacks/pkg.ts
|
|
135
|
+
* import { PkgRegistry } from "@alchemy.run/pkg/Registry";
|
|
136
|
+
* import * as Alchemy from "alchemy";
|
|
137
|
+
* import * as Cloudflare from "alchemy/Cloudflare";
|
|
138
|
+
* import * as ByteSize from "effect/ByteSize";
|
|
139
|
+
* import * as Config from "effect/Config";
|
|
140
|
+
* import * as Duration from "effect/Duration";
|
|
141
|
+
* import * as Effect from "effect/Effect";
|
|
142
|
+
*
|
|
143
|
+
* export default Alchemy.Stack(
|
|
144
|
+
* "Pkg",
|
|
145
|
+
* { providers: Cloudflare.providers(), state: Cloudflare.state() },
|
|
146
|
+
* Effect.gen(function* () {
|
|
147
|
+
* const registry = yield* PkgRegistry("Pkg", {
|
|
148
|
+
* worker: { domain: "pkg.alchemy.run" },
|
|
149
|
+
* github: {
|
|
150
|
+
* appId: Config.String("GH_APP_ID"),
|
|
151
|
+
* privateKey: Config.Redacted("GH_APP_PRIVATE_KEY"),
|
|
152
|
+
* },
|
|
153
|
+
* policy: {
|
|
154
|
+
* repos: ["alchemy-run/alchemy", "alchemy-run/distilled"],
|
|
155
|
+
* ttl: Duration.weeks(1),
|
|
156
|
+
* maxPackageSize: ByteSize.megabytes(100),
|
|
157
|
+
* },
|
|
158
|
+
* });
|
|
159
|
+
* return { url: registry.url.as<string>() };
|
|
160
|
+
* }),
|
|
161
|
+
* );
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @resource
|
|
165
|
+
* @product Workers
|
|
166
|
+
* @category Workers & Compute
|
|
167
|
+
*/
|
|
168
|
+
export const PkgRegistry = <const Id extends string>(
|
|
169
|
+
id: Id,
|
|
170
|
+
options: RegistryOptions,
|
|
171
|
+
) =>
|
|
172
|
+
Registry.pipe(
|
|
173
|
+
Effect.provide(
|
|
174
|
+
RegistryLive.pipe(
|
|
175
|
+
Layer.provide(
|
|
176
|
+
Layer.succeed(RegistryConfig, {
|
|
177
|
+
worker: options.worker ?? {},
|
|
178
|
+
policy: {
|
|
179
|
+
repos: options.policy.repos,
|
|
180
|
+
ttl: options.policy.ttl ?? DEFAULT_TTL,
|
|
181
|
+
...(options.policy.maxPackageSize !== undefined
|
|
182
|
+
? { maxPackageSize: options.policy.maxPackageSize }
|
|
183
|
+
: {}),
|
|
184
|
+
},
|
|
185
|
+
aliases: options.aliases ?? {},
|
|
186
|
+
cron: options.cron ?? DEFAULT_CRON,
|
|
187
|
+
github: {
|
|
188
|
+
apiUrl: options.github.apiUrl ?? DEFAULT_API_URL,
|
|
189
|
+
appId: options.github.appId,
|
|
190
|
+
privateKey: options.github.privateKey,
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
Namespace.push(id),
|
|
197
|
+
);
|
package/src/cli/bin.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { PlatformServices, runMain } from "alchemy/Util/PlatformServices";
|
|
3
|
+
import * as Config from "effect/Config";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import {
|
|
7
|
+
CliConfig,
|
|
8
|
+
CliError,
|
|
9
|
+
Command,
|
|
10
|
+
Flag,
|
|
11
|
+
GlobalFlag,
|
|
12
|
+
} from "effect/unstable/cli";
|
|
13
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
14
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
15
|
+
import { Group, pack } from "./pack.ts";
|
|
16
|
+
import { publish } from "./publish.ts";
|
|
17
|
+
|
|
18
|
+
const groupFlag = Flag.String("group").pipe(
|
|
19
|
+
Flag.withDescription(
|
|
20
|
+
"Display group and directory glob, as NAME=GLOB or NAME[Collapsed]=GLOB (repeatable), e.g. --group Alchemy=./packages/*",
|
|
21
|
+
),
|
|
22
|
+
Flag.withSchema(Group),
|
|
23
|
+
Flag.atLeast(1),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const registryFlag = Flag.String("registry").pipe(
|
|
27
|
+
Flag.withDescription(
|
|
28
|
+
"Registry origin used for install URLs. Falls back to PKG_REGISTRY, then https://pkg.alchemy.run",
|
|
29
|
+
),
|
|
30
|
+
Flag.withFallbackConfig(Config.String("PKG_REGISTRY")),
|
|
31
|
+
Flag.withDefault("https://pkg.alchemy.run"),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const outFlag = Flag.String("out").pipe(
|
|
35
|
+
Flag.withDescription("Directory to write tarballs and the manifest into"),
|
|
36
|
+
Flag.withDefault(".pkg"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const packCommand = Command.make(
|
|
40
|
+
"pack",
|
|
41
|
+
{ group: groupFlag, registry: registryFlag, out: outFlag },
|
|
42
|
+
({ group, registry, out }) =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const cwd = yield* Effect.sync(() => process.cwd());
|
|
45
|
+
yield* pack({ cwd, groups: group, registry, out });
|
|
46
|
+
}),
|
|
47
|
+
).pipe(
|
|
48
|
+
Command.withDescription(
|
|
49
|
+
"Pack workspace packages into reproducible tarballs with dependencies rewritten to registry URLs",
|
|
50
|
+
),
|
|
51
|
+
Command.withExamples([
|
|
52
|
+
{
|
|
53
|
+
command:
|
|
54
|
+
"pkg pack --group 'alchemy=./packages/alchemy' --group '@alchemy.run[Collapsed]=./packages/{better-auth,pkg}' --group '@distilled.cloud[Collapsed]=./submodules/distilled/packages/*'",
|
|
55
|
+
},
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const dirFlag = Flag.String("dir").pipe(
|
|
60
|
+
Flag.withDescription("Directory written by pkg pack"),
|
|
61
|
+
Flag.withDefault(".pkg"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const publishCommand = Command.make(
|
|
65
|
+
"publish",
|
|
66
|
+
{ registry: registryFlag, dir: dirFlag },
|
|
67
|
+
({ registry, dir }) =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const cwd = yield* Effect.sync(() => process.cwd());
|
|
70
|
+
yield* publish({ cwd, dir, registry });
|
|
71
|
+
}),
|
|
72
|
+
).pipe(
|
|
73
|
+
Command.withDescription(
|
|
74
|
+
"Publish a pkg pack directory from the current GitHub Actions job, after its manifest artifact has been uploaded",
|
|
75
|
+
),
|
|
76
|
+
Command.withExamples([
|
|
77
|
+
{ command: "pkg publish --registry https://pkg.alchemy.run" },
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const root = Command.make("pkg", {}, () =>
|
|
82
|
+
Effect.fail(new CliError.ShowHelp({ commandPath: ["pkg"], errors: [] })),
|
|
83
|
+
).pipe(
|
|
84
|
+
Command.withDescription(
|
|
85
|
+
"Pack and publish preview packages for pull requests.",
|
|
86
|
+
),
|
|
87
|
+
Command.withSubcommands([packCommand, publishCommand]),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
Command.run(root, { version: pkg.version }).pipe(
|
|
91
|
+
Effect.provide(
|
|
92
|
+
Layer.mergeAll(
|
|
93
|
+
PlatformServices,
|
|
94
|
+
FetchHttpClient.layer,
|
|
95
|
+
CliConfig.layer({
|
|
96
|
+
builtIns: [GlobalFlag.Help, GlobalFlag.Version],
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
runMain,
|
|
101
|
+
);
|