Created
October 15, 2025 15:20
-
-
Save ericclemmons/22151e28668cc3582a47ad349c4f53fd to your computer and use it in GitHub Desktop.
Alchemy resource for Worker Builds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Resource, type Context } from "alchemy"; | |
| import { CloudflareApi, createCloudflareApi } from "alchemy/cloudflare"; | |
| const DEFAULT_BUILD_COMMAND = ""; | |
| const DEFAULT_PREVIEW_DEPLOY_COMMAND = "npx wrangler versions upload"; | |
| const DEFAULT_PRODUCTION_DEPLOY_COMMAND = "npx wrangler deploy"; | |
| const DEFAULT_ROOT_DIRECTORY = "/"; | |
| interface JsonResponse<T> { | |
| success: boolean; | |
| errors: [unknown[]]; | |
| messages: [unknown[]]; | |
| result: T; | |
| } | |
| interface PaginatedJsonResponse<T> extends JsonResponse<T[]> { | |
| result_info: { | |
| count: number; | |
| next_page: boolean; | |
| page: number; | |
| per_page: number; | |
| total_count: number; | |
| total_pages: number; | |
| }; | |
| } | |
| interface ProviderConnection { | |
| avatar_url: string; | |
| created_on: string; | |
| github_avatar_url: string; | |
| github_username: string; | |
| has_valid_oauth_token: boolean; | |
| owner_display_name: string; | |
| owner: string; | |
| provider_account_id: string; | |
| settings_url: string; | |
| type: string; | |
| } | |
| interface RepoConnection { | |
| default_branch: string; | |
| project_id: string | null; | |
| project_ids: string[] | null; | |
| repo_display_name: string; | |
| repo_id: string; | |
| repo_name: string; | |
| repo_url: string; | |
| } | |
| interface Connection extends Timestamped { | |
| provider_account_id: string; | |
| provider_account_name: string; | |
| provider_type: string; | |
| repo_connection_uuid: string; | |
| repo_id: string; | |
| repo_name: string; | |
| } | |
| interface PutConnection | |
| extends Omit<Connection, keyof Timestamped | "repo_connection_uuid"> {} | |
| interface Token { | |
| build_token_name: string; | |
| build_token_uuid: string; | |
| cloudflare_token_id: string; | |
| owner_type: "user" | unknown; | |
| } | |
| interface Timestamped { | |
| created_on: string; | |
| deleted_on: string | null; | |
| modified_on: string; | |
| } | |
| interface ExternalScript { | |
| /** | |
| * e.g. `props.name` | |
| */ | |
| id: string; | |
| /** | |
| * The actual ID of the worker | |
| */ | |
| tag: string; | |
| entry_point: string; | |
| } | |
| interface Trigger extends Timestamped { | |
| branch_excludes: string[]; | |
| branch_includes: string[]; | |
| build_caching_enabled: boolean; | |
| build_command: string; | |
| build_token_uuid: string; | |
| deploy_command: string; | |
| external_script_id: string; | |
| path_excludes: string[]; | |
| path_includes: string[]; | |
| repo_connection_uuid: string; | |
| root_directory: string; | |
| trigger_name: string; | |
| trigger_uuid: string; | |
| } | |
| interface PutTrigger | |
| extends Omit<Trigger, keyof Timestamped | "trigger_uuid"> {} | |
| export interface WorkerProjectProps { | |
| /** | |
| * @default "" (inferrred by Cloudflare) | |
| */ | |
| buildCommand?: string; | |
| /** | |
| * @default "npx wrangler deploy" | |
| */ | |
| deployCommand?: string; | |
| /** | |
| * @default "/" | |
| */ | |
| rootDirectory?: string; | |
| /** | |
| * Should be the name of the Worker (e.g. `wrangler.jsonc#name`) | |
| */ | |
| name: string; | |
| // TODO: Support logs | |
| // PATCH https://dash.cloudflare.com/api/v4/accounts/${app.accountId}/workers/scripts/${app.name}/script-settings | |
| // { "logs": { "enabled": true, "head_sampling_rate": 1, "invocation_logs": true } } | |
| owner: string; | |
| /** | |
| * @default true | |
| */ | |
| preview?: Pick< | |
| WorkerProjectProps, | |
| "buildCommand" | "deployCommand" | "rootDirectory" | |
| >; | |
| /** | |
| * @default "github" | |
| */ | |
| provider?: "github"; | |
| repo: string; | |
| } | |
| export interface WorkerProject | |
| extends Resource<"cloudflare::WorkerProject">, | |
| WorkerProjectProps { | |
| /** | |
| * Production and (optional) preview triggers | |
| */ | |
| triggers: [Trigger, Trigger]; | |
| } | |
| const getProviderConnection = async ( | |
| api: CloudflareApi, | |
| props: WorkerProjectProps | |
| ) => { | |
| const { provider = "github", owner, repo } = props; | |
| const response = await api.get( | |
| `/accounts/${api.accountId}/pages/connections` | |
| ); | |
| if (!response.ok) { | |
| throw new Error("Could not get Cloudflare connections", { | |
| cause: response, | |
| }); | |
| } | |
| const connections = (await response.json()) as JsonResponse< | |
| ProviderConnection[] | |
| >; | |
| if (!connections.success) { | |
| throw new Error( | |
| "Unexpected Error – Cloudflare did not return connections from the API", | |
| { cause: connections } | |
| ); | |
| } | |
| const providerConnection = connections.result.find( | |
| (connection) => connection.type === provider && connection.owner === owner | |
| ); | |
| if (!providerConnection) { | |
| throw new Error( | |
| `Cloudflare does not have access to "${owner}/${repo}". Make sure "${owner}/${repo}" (NOT "All Repositories") is selected at:\n\thttps://github.com/apps/cloudflare-workers-and-pages/installations/select_target`, | |
| { cause: connections } | |
| ); | |
| } | |
| return providerConnection; | |
| }; | |
| const getRepoConnection = async ( | |
| api: CloudflareApi, | |
| props: WorkerProjectProps | |
| ) => { | |
| const { provider = "github", owner, repo } = props; | |
| const response = await api.get( | |
| `/accounts/${api.accountId}/pages/connections/${provider}/${owner}/repos/${repo}` | |
| ); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Cloudflare does not have access to "${owner}/${repo}". Make sure "${owner}/${repo}" (NOT "All Repositories") is selected at:\n\thttps://github.com/apps/cloudflare-workers-and-pages/installations/select_target`, | |
| { cause: response } | |
| ); | |
| } | |
| const connection = (await response.json()) as JsonResponse<RepoConnection>; | |
| if (!connection.success) { | |
| throw new Error( | |
| `Unexpected Error – Cloudflare has access to "${owner}/${repo}", but did not return it from the API`, | |
| { cause: connection } | |
| ); | |
| } | |
| return connection.result; | |
| }; | |
| const getConnection = async (api: CloudflareApi, props: WorkerProjectProps) => { | |
| const [provider, repo] = await Promise.all([ | |
| getProviderConnection(api, props), | |
| getRepoConnection(api, props), | |
| ]); | |
| return { provider, repo }; | |
| }; | |
| const getBuildToken = async (api: CloudflareApi, props: WorkerProjectProps) => { | |
| const response = await api.get(`/accounts/${api.accountId}/builds/tokens`); | |
| if (!response.ok) { | |
| throw new Error("Could not get Cloudflare build token", { | |
| cause: response, | |
| }); | |
| } | |
| const tokens = (await response.json()) as PaginatedJsonResponse<Token>; | |
| const [token] = tokens.result; | |
| if (!token) { | |
| throw new Error("Unexpected Error – Could not get Cloudflare build token", { | |
| cause: tokens, | |
| }); | |
| } | |
| return token; | |
| }; | |
| const createExternalScript = async ( | |
| api: CloudflareApi, | |
| props: WorkerProjectProps | |
| ) => { | |
| const body = new FormData(); | |
| // Create a worker stub to satisfy an initial build | |
| body.append( | |
| "worker.js", | |
| new Blob( | |
| [ | |
| `export default { async fetch(request, env) { return new Response("Hello world") } }`, | |
| ], | |
| { type: "application/javascript+module" } | |
| ), | |
| "worker.js" | |
| ); | |
| body.append( | |
| "metadata", | |
| new Blob( | |
| [ | |
| JSON.stringify({ | |
| compatibility_date: "2025-06-07", | |
| keep_assets: true, | |
| bindings: [], | |
| main_module: "worker.js", | |
| }), | |
| ], | |
| { type: "application/json" } | |
| ), | |
| "blob" | |
| ); | |
| const response = await api.put( | |
| `/accounts/${api.accountId}/workers/services/${props.name}/environments/production?include_subdomain_availability=true`, | |
| body, | |
| { | |
| headers: { | |
| "Content-Type": "multipart/form-data", | |
| }, | |
| } | |
| ); | |
| if (!response.ok) { | |
| throw new Error("Could not create external script", { cause: response }); | |
| } | |
| const { result } = (await response.json()) as JsonResponse<ExternalScript>; | |
| return result; | |
| }; | |
| const createConnection = async ( | |
| api: CloudflareApi, | |
| { provider, repo }: Awaited<ReturnType<typeof getConnection>> | |
| ) => { | |
| const response = await api.put( | |
| `/accounts/${api.accountId}/builds/repos/connections`, | |
| { | |
| provider_account_id: provider.provider_account_id, | |
| provider_account_name: provider.owner, | |
| provider_type: provider.type, | |
| repo_id: repo.repo_id, | |
| repo_name: repo.repo_name, | |
| } satisfies PutConnection | |
| ); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Could not Cloudflare connection to ${provider.owner}/${repo.repo_name}`, | |
| { cause: response } | |
| ); | |
| } | |
| const { result } = (await response.json()) as JsonResponse<Connection>; | |
| return result; | |
| }; | |
| const createWorkerProject = async ( | |
| api: CloudflareApi, | |
| props: WorkerProjectProps | |
| ): Promise<WorkerProject["triggers"]> => { | |
| const { provider, repo } = await getConnection(api, props); | |
| const connection = await createConnection(api, { provider, repo }); | |
| const buildToken = await getBuildToken(api, props); | |
| const externalScript = await createExternalScript(api, props); | |
| const productionTrigger = (await api | |
| .post(`/accounts/${api.accountId}/builds/triggers`, { | |
| branch_excludes: [], | |
| branch_includes: [repo.default_branch], | |
| build_caching_enabled: true, | |
| build_command: props.buildCommand ?? DEFAULT_BUILD_COMMAND, | |
| build_token_uuid: buildToken.build_token_uuid, | |
| deploy_command: props.deployCommand ?? DEFAULT_PRODUCTION_DEPLOY_COMMAND, | |
| external_script_id: externalScript.tag, | |
| path_excludes: [], | |
| path_includes: ["*"], | |
| repo_connection_uuid: connection.repo_connection_uuid, | |
| root_directory: props.rootDirectory ?? DEFAULT_ROOT_DIRECTORY, | |
| trigger_name: "Deploy default branch", | |
| } satisfies PutTrigger) | |
| .then((response) => response.json())) as JsonResponse<Trigger>; | |
| // Prefer preview values over props values | |
| const { buildCommand, deployCommand, rootDirectory } = { | |
| ...props, | |
| ...props.preview, | |
| }; | |
| const previewResponse = (await api | |
| .post(`/accounts/${api.accountId}/builds/triggers`, { | |
| branch_excludes: [repo.default_branch], | |
| branch_includes: ["*"], | |
| build_caching_enabled: true, | |
| build_command: buildCommand ?? DEFAULT_BUILD_COMMAND, | |
| build_token_uuid: buildToken.build_token_uuid, | |
| deploy_command: deployCommand ?? DEFAULT_PREVIEW_DEPLOY_COMMAND, | |
| external_script_id: externalScript.tag, | |
| path_excludes: [], | |
| path_includes: ["*"], | |
| repo_connection_uuid: connection.repo_connection_uuid, | |
| root_directory: rootDirectory ?? DEFAULT_ROOT_DIRECTORY, | |
| trigger_name: "Deploy non-production branches", | |
| } satisfies PutTrigger) | |
| .then((response) => response.json())) as JsonResponse<Trigger>; | |
| // Enable *.workers.dev | |
| await api.post( | |
| `/accounts/${api.accountId}/workers/services/${props.name}/environments/production/subdomain`, | |
| { enabled: true, previews_enabled: true } | |
| ); | |
| return [productionTrigger.result, previewResponse.result]; | |
| }; | |
| const updateWorkerProject = async ( | |
| api: CloudflareApi, | |
| props: WorkerProjectProps, | |
| output: WorkerProject | |
| ): Promise<WorkerProject["triggers"]> => { | |
| const [previousProductionTrigger, previousPreviewTrigger] = output.triggers; | |
| const productionTrigger = (await api | |
| .patch( | |
| `/accounts/${api.accountId}/builds/triggers/${previousProductionTrigger.trigger_uuid}`, | |
| { | |
| build_command: props.buildCommand ?? DEFAULT_BUILD_COMMAND, | |
| deploy_command: | |
| props.deployCommand ?? DEFAULT_PRODUCTION_DEPLOY_COMMAND, | |
| root_directory: props.rootDirectory ?? DEFAULT_ROOT_DIRECTORY, | |
| } satisfies Partial<PutTrigger> | |
| ) | |
| .then((response) => response.json())) as JsonResponse<Trigger>; | |
| const previewTrigger = (await api | |
| .patch( | |
| `/accounts/${api.accountId}/builds/triggers/${previousPreviewTrigger.trigger_uuid}`, | |
| { | |
| build_command: props.buildCommand ?? DEFAULT_BUILD_COMMAND, | |
| deploy_command: props.deployCommand ?? DEFAULT_PREVIEW_DEPLOY_COMMAND, | |
| root_directory: props.rootDirectory ?? DEFAULT_ROOT_DIRECTORY, | |
| } satisfies Partial<PutTrigger> | |
| ) | |
| .then((response) => response.json())) as JsonResponse<Trigger>; | |
| return [productionTrigger.result, previewTrigger.result]; | |
| }; | |
| export const WorkerProject = Resource( | |
| "cloudflare::WorkerProject", | |
| async function ( | |
| this: Context<WorkerProject>, | |
| id: string, | |
| props: WorkerProjectProps | |
| ): Promise<WorkerProject> { | |
| const api = await createCloudflareApi(); | |
| switch (this.phase) { | |
| case "create": | |
| return this.create({ | |
| ...props, | |
| triggers: await createWorkerProject(api, props), | |
| }); | |
| case "update": | |
| return this.create({ | |
| ...props, | |
| triggers: await updateWorkerProject(api, props, this.output), | |
| }); | |
| case "delete": | |
| const response = await api.delete( | |
| `/accounts/${api.accountId}/workers/services/${ | |
| this.output.name ?? props.name | |
| }?force=false` | |
| ); | |
| if (!response.ok) { | |
| console.warn( | |
| `Could not delete Cloudflare Worker Project through API. Do it manually at:\n\thttps://dash.cloudflare.com/${api.accountId}/workers-and-pages` | |
| ); | |
| } | |
| return this.destroy(); | |
| default: | |
| const phase: never = this.phase; | |
| throw new Error(`Unsupported phase: ${phase}`); | |
| } | |
| } | |
| ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment