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.
- package/bin/alchemy-effect.js +2 -2
- package/bin/alchemy-effect.js.map +1 -1
- package/package.json +18 -9
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +3 -0
- package/src/Cloudflare/Container/ContainerBinding.ts +1 -1
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/R2/R2Bucket.ts +43 -0
- package/src/Cloudflare/Website/StaticSite.ts +57 -0
- package/src/Cloudflare/Website/Vite.ts +52 -8
- package/src/Cloudflare/Workers/{DurableObject.ts → DurableObjectNamespace.ts} +242 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +312 -7
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +1 -1
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +212 -0
- package/src/GitHub/Variable.ts +123 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Provider.ts +1 -1
- package/src/Util/dedent.ts +56 -0
- package/src/Util/index.ts +1 -0
|
@@ -186,30 +186,72 @@ export class WorkflowScope extends Context.Service<
|
|
|
186
186
|
>()("Cloudflare.Workflow") {}
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
*
|
|
189
|
+
* A Cloudflare Workflow that orchestrates durable, multi-step tasks with
|
|
190
|
+
* automatic retries and at-least-once delivery.
|
|
190
191
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
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>]
|
|
@@ -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
|
+
});
|
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
|
|
372
|
+
return direct;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
const context = yield* Effect.context<never>();
|