alchemy 2.0.0-beta.2 → 2.0.0-beta.3

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.
@@ -186,30 +186,72 @@ export class WorkflowScope extends Context.Service<
186
186
  >()("Cloudflare.Workflow") {}
187
187
 
188
188
  /**
189
- * Declare a Cloudflare Workflow inside a Worker program.
189
+ * A Cloudflare Workflow that orchestrates durable, multi-step tasks with
190
+ * automatic retries and at-least-once delivery.
190
191
  *
191
- * The outer Effect resolves infrastructure dependencies (Durable Objects,
192
- * etc.) and returns the workflow body -- an Effect that uses `WorkflowEvent`
193
- * and step primitives (`task`, `sleep`, `sleepUntil`).
192
+ * A Workflow follows the same two-phase pattern as Workers and Durable
193
+ * Objects. The outer `Effect.gen` resolves shared dependencies. The inner
194
+ * `Effect.gen` is the workflow body — it reads the triggering event and
195
+ * runs steps using `task`, `sleep`, and `sleepUntil`.
194
196
  *
195
- * Internally this creates a `WorkflowResource` that manages the Cloudflare
196
- * Workflows API lifecycle (PUT / DELETE), similar to how `bindContainer`
197
- * creates a `ContainerApplication`.
197
+ * ```typescript
198
+ * Effect.gen(function* () {
199
+ * // Phase 1: resolve dependencies
200
+ * const notifier = yield* NotificationService;
201
+ *
202
+ * return Effect.gen(function* () {
203
+ * // Phase 2: workflow body (durable steps)
204
+ * const event = yield* Cloudflare.WorkflowEvent;
205
+ * const result = yield* Cloudflare.task("process", doWork(event.payload));
206
+ * yield* Cloudflare.sleep("cooldown", "10 seconds");
207
+ * return result;
208
+ * });
209
+ * })
210
+ * ```
211
+ *
212
+ * @resource
198
213
  *
199
- * @example
214
+ * @section Defining a Workflow
215
+ * @example Minimal workflow
200
216
  * ```typescript
201
217
  * export default class MyWorkflow extends Cloudflare.Workflow<MyWorkflow>()(
202
218
  * "MyWorkflow",
203
219
  * Effect.gen(function* () {
204
220
  * return Effect.gen(function* () {
205
221
  * const event = yield* Cloudflare.WorkflowEvent;
206
- * const data = yield* Cloudflare.task("fetch-data", Effect.succeed({ ok: true }));
207
- * yield* Cloudflare.sleep("pause", "5 seconds");
208
- * return data;
222
+ * return { received: event.payload };
209
223
  * });
210
224
  * }),
211
225
  * ) {}
212
226
  * ```
227
+ *
228
+ * @section Step Primitives
229
+ * @example Running a named task
230
+ * ```typescript
231
+ * const result = yield* Cloudflare.task(
232
+ * "process-order",
233
+ * Effect.succeed({ orderId: "abc", total: 42 }),
234
+ * );
235
+ * ```
236
+ *
237
+ * @example Sleeping between steps
238
+ * ```typescript
239
+ * yield* Cloudflare.sleep("cooldown", "30 seconds");
240
+ * ```
241
+ *
242
+ * @section Starting and Monitoring Instances
243
+ * @example Creating an instance from a Worker
244
+ * ```typescript
245
+ * const workflow = yield* MyWorkflow;
246
+ * const instance = yield* workflow.create({ orderId: "abc" });
247
+ * ```
248
+ *
249
+ * @example Checking instance status
250
+ * ```typescript
251
+ * const workflow = yield* MyWorkflow;
252
+ * const handle = yield* workflow.get(instanceId);
253
+ * const status = yield* handle.status();
254
+ * ```
213
255
  */
214
256
  export const Workflow: WorkflowClass = taggedFunction(WorkflowScope, ((
215
257
  ...args: [] | [name: string, impl: Effect.Effect<WorkflowBody>]
@@ -1,6 +1,6 @@
1
1
  export * from "./Assets.ts";
2
2
  export * from "./ConfigProvider.ts";
3
- export * from "./DurableObject.ts";
3
+ export * from "./DurableObjectNamespace.ts";
4
4
  export * from "./DynamicWorkerLoader.ts";
5
5
  export * from "./Fetch.ts";
6
6
  export * from "./HttpServer.ts";
@@ -0,0 +1,224 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+ import { dedent } from "../Util/dedent.ts";
6
+
7
+ export interface CommentProps {
8
+ /**
9
+ * Repository owner (user or organization).
10
+ */
11
+ owner: string;
12
+
13
+ /**
14
+ * Repository name.
15
+ */
16
+ repository: string;
17
+
18
+ /**
19
+ * Issue or Pull Request number to comment on.
20
+ */
21
+ issueNumber: number;
22
+
23
+ /**
24
+ * Comment body (supports GitHub Markdown).
25
+ *
26
+ * The body is automatically dedented, so you can use indented template
27
+ * literals without worrying about leading whitespace. Accepts
28
+ * `Output<string>` at the call site via `Output.interpolate` to embed
29
+ * resource attributes that are not yet resolved.
30
+ */
31
+ body: string;
32
+
33
+ /**
34
+ * Whether to allow deletion of the comment when the resource is destroyed.
35
+ * By default, comments are never deleted to preserve discussion history.
36
+ * @default false
37
+ */
38
+ allowDelete?: boolean;
39
+
40
+ /**
41
+ * GitHub API token. If not provided, falls back to
42
+ * `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
43
+ */
44
+ token?: string;
45
+ }
46
+
47
+ export interface Comment extends Resource<
48
+ "GitHub.Comment",
49
+ CommentProps,
50
+ {
51
+ /**
52
+ * The numeric ID of the comment in GitHub.
53
+ */
54
+ commentId: number;
55
+
56
+ /**
57
+ * URL to view the comment in a browser.
58
+ */
59
+ htmlUrl: string;
60
+
61
+ /**
62
+ * ISO-8601 timestamp of the last update.
63
+ */
64
+ updatedAt: string;
65
+ }
66
+ > {}
67
+
68
+ /**
69
+ * A GitHub issue or pull request comment.
70
+ *
71
+ * `Comment` manages the lifecycle of a single comment on an issue or pull
72
+ * request. Comments are created on the first deploy and updated in place on
73
+ * subsequent deploys when the `body` changes. By default, comments are never
74
+ * deleted to preserve discussion history — set `allowDelete: true` to opt in.
75
+ *
76
+ * Authentication is resolved in order: explicit `token` prop,
77
+ * `GITHUB_ACCESS_TOKEN` env var, `GITHUB_TOKEN` env var. The token needs
78
+ * `repo` scope for private repositories or `public_repo` for public ones.
79
+ *
80
+ * @section Creating Comments
81
+ * @example Comment on an Issue
82
+ * ```typescript
83
+ * const comment = yield* GitHub.Comment("issue-comment", {
84
+ * owner: "my-org",
85
+ * repository: "my-repo",
86
+ * issueNumber: 123,
87
+ * body: "This is a comment created by Alchemy!",
88
+ * });
89
+ * ```
90
+ *
91
+ * @example Comment on a Pull Request
92
+ * ```typescript
93
+ * const prComment = yield* GitHub.Comment("pr-comment", {
94
+ * owner: "my-org",
95
+ * repository: "my-repo",
96
+ * issueNumber: 456,
97
+ * body: "## Deployment Status\n\nSuccessfully deployed to staging!",
98
+ * });
99
+ * ```
100
+ *
101
+ * @section Updating Comments
102
+ * Deploy with the same logical ID and a different `body` to update the
103
+ * existing comment in place rather than creating a new one.
104
+ *
105
+ * @example Update Comment Content
106
+ * ```typescript
107
+ * const comment = yield* GitHub.Comment("status-comment", {
108
+ * owner: "my-org",
109
+ * repository: "my-repo",
110
+ * issueNumber: 789,
111
+ * body: "Deployment completed successfully!",
112
+ * });
113
+ * ```
114
+ *
115
+ * @section Deleting Comments
116
+ * @example Allow Comment Deletion
117
+ * ```typescript
118
+ * const comment = yield* GitHub.Comment("temp-comment", {
119
+ * owner: "my-org",
120
+ * repository: "my-repo",
121
+ * issueNumber: 123,
122
+ * body: "This comment can be deleted",
123
+ * allowDelete: true,
124
+ * });
125
+ * ```
126
+ *
127
+ * @section CI Preview Comments
128
+ * A common pattern is posting a preview-deployment URL on every pull request.
129
+ * The comment auto-updates on each push because the logical ID stays the same.
130
+ *
131
+ * @example PR Preview Comment
132
+ * ```typescript
133
+ * if (process.env.PULL_REQUEST) {
134
+ * yield* GitHub.Comment("preview-comment", {
135
+ * owner: "my-org",
136
+ * repository: "my-repo",
137
+ * issueNumber: Number(process.env.PULL_REQUEST),
138
+ * body: Output.interpolate`
139
+ * ## Preview Deployed
140
+ *
141
+ * **URL:** ${website.url}
142
+ * `,
143
+ * });
144
+ * }
145
+ * ```
146
+ */
147
+ export const Comment = Resource<Comment>("GitHub.Comment");
148
+
149
+ function resolveToken(props: CommentProps): string | undefined {
150
+ return (
151
+ props.token ?? process.env.GITHUB_ACCESS_TOKEN ?? process.env.GITHUB_TOKEN
152
+ );
153
+ }
154
+
155
+ function createClient(props: CommentProps): Octokit {
156
+ return new Octokit({ auth: resolveToken(props) });
157
+ }
158
+
159
+ export const CommentProvider = () =>
160
+ Provider.succeed(Comment, {
161
+ stables: ["commentId"],
162
+
163
+ create: Effect.fn(function* ({ news }) {
164
+ const octokit = createClient(news);
165
+ const body = dedent(news.body);
166
+
167
+ const { data } = yield* Effect.tryPromise(() =>
168
+ octokit.rest.issues.createComment({
169
+ owner: news.owner,
170
+ repo: news.repository,
171
+ issue_number: news.issueNumber,
172
+ body,
173
+ }),
174
+ );
175
+
176
+ return {
177
+ commentId: data.id,
178
+ htmlUrl: data.html_url,
179
+ updatedAt: data.updated_at,
180
+ };
181
+ }),
182
+
183
+ update: Effect.fn(function* ({ news, output }) {
184
+ const octokit = createClient(news);
185
+ const body = dedent(news.body);
186
+
187
+ const { data } = yield* Effect.tryPromise(() =>
188
+ octokit.rest.issues.updateComment({
189
+ owner: news.owner,
190
+ repo: news.repository,
191
+ comment_id: output.commentId,
192
+ body,
193
+ }),
194
+ );
195
+
196
+ return {
197
+ commentId: data.id,
198
+ htmlUrl: data.html_url,
199
+ updatedAt: data.updated_at,
200
+ };
201
+ }),
202
+
203
+ delete: Effect.fn(function* ({ olds, output }) {
204
+ if (!olds.allowDelete) {
205
+ return;
206
+ }
207
+
208
+ const octokit = createClient(olds);
209
+
210
+ yield* Effect.tryPromise(async () => {
211
+ try {
212
+ await octokit.rest.issues.deleteComment({
213
+ owner: olds.owner,
214
+ repo: olds.repository,
215
+ comment_id: output.commentId,
216
+ });
217
+ } catch (error: any) {
218
+ if (error.status !== 404) {
219
+ throw error;
220
+ }
221
+ }
222
+ });
223
+ }),
224
+ });
@@ -0,0 +1,212 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import * as Provider from "../Provider.ts";
5
+ import { Resource } from "../Resource.ts";
6
+
7
+ export interface SecretProps {
8
+ /**
9
+ * Repository owner (user or organization).
10
+ */
11
+ owner: string;
12
+
13
+ /**
14
+ * Repository name.
15
+ */
16
+ repository: string;
17
+
18
+ /**
19
+ * Secret name (e.g. `AWS_ROLE_ARN`).
20
+ */
21
+ name: string;
22
+
23
+ /**
24
+ * Secret value. Wrap with `Redacted.make` to prevent the value from
25
+ * appearing in logs or state.
26
+ */
27
+ value: Redacted.Redacted;
28
+
29
+ /**
30
+ * Optional environment name. When set the secret is scoped to that
31
+ * GitHub Actions environment instead of the whole repository.
32
+ */
33
+ environment?: string;
34
+
35
+ /**
36
+ * GitHub API token. If not provided, falls back to
37
+ * `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
38
+ */
39
+ token?: string;
40
+ }
41
+
42
+ export interface Secret extends Resource<
43
+ "GitHub.Secret",
44
+ SecretProps,
45
+ {
46
+ /**
47
+ * ISO-8601 timestamp of the last update.
48
+ */
49
+ updatedAt: string;
50
+ }
51
+ > {}
52
+
53
+ /**
54
+ * A GitHub Actions repository or environment secret.
55
+ *
56
+ * Secrets are encrypted using the repository's (or environment's) public
57
+ * key before being stored. The resource is idempotent — calling it with
58
+ * the same name will update the secret value in place.
59
+ *
60
+ * @section Repository Secrets
61
+ * @example Create a Repository Secret
62
+ * ```typescript
63
+ * yield* GitHub.Secret("aws-role", {
64
+ * owner: "my-org",
65
+ * repository: "my-repo",
66
+ * name: "AWS_ROLE_ARN",
67
+ * value: Redacted.make(role.roleArn),
68
+ * });
69
+ * ```
70
+ *
71
+ * @section Environment Secrets
72
+ * @example Create an Environment Secret
73
+ * ```typescript
74
+ * yield* GitHub.Secret("deploy-key", {
75
+ * owner: "my-org",
76
+ * repository: "my-repo",
77
+ * environment: "production",
78
+ * name: "DEPLOY_KEY",
79
+ * value: Redacted.make("my-secret-value"),
80
+ * });
81
+ * ```
82
+ */
83
+ export const Secret = Resource<Secret>("GitHub.Secret");
84
+
85
+ function resolveToken(props: SecretProps): string | undefined {
86
+ return (
87
+ props.token ?? process.env.GITHUB_ACCESS_TOKEN ?? process.env.GITHUB_TOKEN
88
+ );
89
+ }
90
+
91
+ function createClient(props: SecretProps): Octokit {
92
+ return new Octokit({ auth: resolveToken(props) });
93
+ }
94
+
95
+ async function encryptValue(
96
+ plaintext: string,
97
+ publicKey: string,
98
+ ): Promise<string> {
99
+ const sodium = await import("libsodium-wrappers");
100
+ await sodium.ready;
101
+ const binKey = sodium.from_base64(
102
+ publicKey,
103
+ sodium.base64_variants.ORIGINAL,
104
+ );
105
+ const binMessage = sodium.from_string(plaintext);
106
+ const encrypted = sodium.crypto_box_seal(binMessage, binKey);
107
+ return sodium.to_base64(encrypted, sodium.base64_variants.ORIGINAL);
108
+ }
109
+
110
+ export const SecretProvider = () =>
111
+ Provider.succeed(Secret, {
112
+ create: Effect.fn(function* ({ news }) {
113
+ const octokit = createClient(news);
114
+ yield* upsertSecret(octokit, news);
115
+ return { updatedAt: new Date().toISOString() };
116
+ }),
117
+
118
+ update: Effect.fn(function* ({ news, olds, output }) {
119
+ const octokit = createClient(news);
120
+
121
+ const wasEnv = !!olds.environment;
122
+ const isEnv = !!news.environment;
123
+ if (wasEnv !== isEnv || olds.environment !== news.environment) {
124
+ yield* deleteSecret(octokit, olds);
125
+ }
126
+
127
+ yield* upsertSecret(octokit, news);
128
+ return { updatedAt: new Date().toISOString() };
129
+ }),
130
+
131
+ delete: Effect.fn(function* ({ olds }) {
132
+ const octokit = createClient(olds);
133
+ yield* deleteSecret(octokit, olds);
134
+ }),
135
+ });
136
+
137
+ const upsertSecret = Effect.fn(function* (
138
+ octokit: Octokit,
139
+ props: SecretProps,
140
+ ) {
141
+ const plaintext = Redacted.value(props.value);
142
+ const isEnv = !!props.environment;
143
+
144
+ const publicKey = yield* Effect.tryPromise(async () => {
145
+ if (isEnv) {
146
+ const { data } = await octokit.rest.actions.getEnvironmentPublicKey({
147
+ owner: props.owner,
148
+ repo: props.repository,
149
+ environment_name: props.environment!,
150
+ });
151
+ return data;
152
+ }
153
+ const { data } = await octokit.rest.actions.getRepoPublicKey({
154
+ owner: props.owner,
155
+ repo: props.repository,
156
+ });
157
+ return data;
158
+ });
159
+
160
+ const encrypted = yield* Effect.tryPromise(() =>
161
+ encryptValue(plaintext, publicKey.key),
162
+ );
163
+
164
+ yield* Effect.tryPromise(async () => {
165
+ if (isEnv) {
166
+ await octokit.rest.actions.createOrUpdateEnvironmentSecret({
167
+ owner: props.owner,
168
+ repo: props.repository,
169
+ environment_name: props.environment!,
170
+ secret_name: props.name,
171
+ encrypted_value: encrypted,
172
+ key_id: publicKey.key_id,
173
+ });
174
+ } else {
175
+ await octokit.rest.actions.createOrUpdateRepoSecret({
176
+ owner: props.owner,
177
+ repo: props.repository,
178
+ secret_name: props.name,
179
+ encrypted_value: encrypted,
180
+ key_id: publicKey.key_id,
181
+ });
182
+ }
183
+ });
184
+ });
185
+
186
+ const deleteSecret = Effect.fn(function* (
187
+ octokit: Octokit,
188
+ props: SecretProps,
189
+ ) {
190
+ yield* Effect.tryPromise(async () => {
191
+ try {
192
+ if (props.environment) {
193
+ await octokit.rest.actions.deleteEnvironmentSecret({
194
+ owner: props.owner,
195
+ repo: props.repository,
196
+ environment_name: props.environment,
197
+ secret_name: props.name,
198
+ });
199
+ } else {
200
+ await octokit.rest.actions.deleteRepoSecret({
201
+ owner: props.owner,
202
+ repo: props.repository,
203
+ secret_name: props.name,
204
+ });
205
+ }
206
+ } catch (error: any) {
207
+ if (error.status !== 404) {
208
+ throw error;
209
+ }
210
+ }
211
+ });
212
+ });
@@ -0,0 +1,123 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Provider from "../Provider.ts";
4
+ import { Resource } from "../Resource.ts";
5
+
6
+ export interface VariableProps {
7
+ /**
8
+ * Repository owner (user or organization).
9
+ */
10
+ owner: string;
11
+
12
+ /**
13
+ * Repository name.
14
+ */
15
+ repository: string;
16
+
17
+ /**
18
+ * Variable name (e.g. `AWS_ROLE_ARN`).
19
+ */
20
+ name: string;
21
+
22
+ /**
23
+ * Variable value.
24
+ */
25
+ value: string;
26
+
27
+ /**
28
+ * GitHub API token. If not provided, falls back to
29
+ * `GITHUB_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variables.
30
+ */
31
+ token?: string;
32
+ }
33
+
34
+ export interface Variable extends Resource<
35
+ "GitHub.Variable",
36
+ VariableProps,
37
+ {
38
+ /**
39
+ * ISO-8601 timestamp of the last update.
40
+ */
41
+ updatedAt: string;
42
+ }
43
+ > {}
44
+
45
+ /**
46
+ * A GitHub Actions repository variable.
47
+ *
48
+ * Variables are stored in plain text and are suitable for non-sensitive
49
+ * configuration like region names or role ARNs.
50
+ *
51
+ * @section Repository Variables
52
+ * @example Create a Repository Variable
53
+ * ```typescript
54
+ * yield* GitHub.Variable("aws-region", {
55
+ * owner: "my-org",
56
+ * repository: "my-repo",
57
+ * name: "AWS_REGION",
58
+ * value: "us-east-1",
59
+ * });
60
+ * ```
61
+ */
62
+ export const Variable = Resource<Variable>("GitHub.Variable");
63
+
64
+ function resolveToken(props: VariableProps): string | undefined {
65
+ return (
66
+ props.token ?? process.env.GITHUB_ACCESS_TOKEN ?? process.env.GITHUB_TOKEN
67
+ );
68
+ }
69
+
70
+ function createClient(props: VariableProps): Octokit {
71
+ return new Octokit({ auth: resolveToken(props) });
72
+ }
73
+
74
+ export const VariableProvider = () =>
75
+ Provider.succeed(Variable, {
76
+ create: Effect.fn(function* ({ news }) {
77
+ const octokit = createClient(news);
78
+
79
+ yield* Effect.tryPromise(() =>
80
+ octokit.rest.actions.createRepoVariable({
81
+ owner: news.owner,
82
+ repo: news.repository,
83
+ name: news.name,
84
+ value: news.value,
85
+ }),
86
+ );
87
+
88
+ return { updatedAt: new Date().toISOString() };
89
+ }),
90
+
91
+ update: Effect.fn(function* ({ news }) {
92
+ const octokit = createClient(news);
93
+
94
+ yield* Effect.tryPromise(() =>
95
+ octokit.rest.actions.updateRepoVariable({
96
+ owner: news.owner,
97
+ repo: news.repository,
98
+ name: news.name,
99
+ value: news.value,
100
+ }),
101
+ );
102
+
103
+ return { updatedAt: new Date().toISOString() };
104
+ }),
105
+
106
+ delete: Effect.fn(function* ({ olds }) {
107
+ const octokit = createClient(olds);
108
+
109
+ yield* Effect.tryPromise(async () => {
110
+ try {
111
+ await octokit.rest.actions.deleteRepoVariable({
112
+ owner: olds.owner,
113
+ repo: olds.repository,
114
+ name: olds.name,
115
+ });
116
+ } catch (error: any) {
117
+ if (error.status !== 404) {
118
+ throw error;
119
+ }
120
+ }
121
+ });
122
+ }),
123
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./Comment.ts";
2
+ export * from "./Secret.ts";
3
+ export * from "./Variable.ts";
package/src/Provider.ts CHANGED
@@ -369,7 +369,7 @@ export const tryFindProviderByType: {
369
369
  const Tag = Provider<R>(resourceType) as Context.Service<Provider<R>, any>;
370
370
  const direct = yield* Effect.serviceOption(Tag);
371
371
  if (Option.isSome(direct)) {
372
- return direct.value;
372
+ return direct;
373
373
  }
374
374
 
375
375
  const context = yield* Effect.context<never>();