Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions services/deploy-infra/builder/scripts/deploy-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ function loadEnvFile(): Record<string, string> {
const key = trimmed.slice(0, eqIndex);
let value = trimmed.slice(eqIndex + 1);

// Remove surrounding quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
Expand Down Expand Up @@ -145,13 +144,11 @@ async function main() {
const slug = parsed.slug || path.basename(directory);
const envVars = parsed.envVars;

// Validate directory exists
if (!fs.existsSync(directory)) {
console.error(`Error: Directory not found: ${directory}`);
process.exit(1);
}

// Validate directory is actually a directory
if (!fs.statSync(directory).isDirectory()) {
console.error(`Error: Not a directory: ${directory}`);
process.exit(1);
Expand All @@ -169,12 +166,10 @@ async function main() {
console.log('='.repeat(60));
console.log('');

// Create tar.gz archive in temp location
const archivePath = `/tmp/${slug}-${Date.now()}.tar.gz`;

console.log('📦 Creating archive...');

// Build exclusion list for tar command
const excludePatterns = [
'.next',
'node_modules', // Dependencies
Expand Down Expand Up @@ -212,7 +207,6 @@ async function main() {
console.log('');

try {
// Upload to builder
console.log('🚀 Uploading to builder...');
const archiveData = fs.readFileSync(archivePath);

Expand Down Expand Up @@ -246,13 +240,11 @@ async function main() {
console.log(` Build ID: ${result.buildId}`);
console.log('');

// Poll for status
console.log('⏳ Building...');
let lastStatus = '';
let lastEventCount = 0;

while (true) {
// Fetch and display new events
const eventsResponse = await fetch(`${BUILDER_URL}/deploy/${result.buildId}/events`, {
headers: { Authorization: `Bearer ${BUILDER_AUTH_TOKEN}` },
});
Expand All @@ -262,7 +254,6 @@ async function main() {
payload: { status?: string };
}[];

// Show new log events
for (let i = lastEventCount; i < events.length; i++) {
const event = events[i];
console.log(` ${JSON.stringify(event.payload)}`);
Expand All @@ -273,7 +264,6 @@ async function main() {
}
lastEventCount = events.length;

// Check if finished
if (['deployed', 'failed', 'cancelled'].includes(lastStatus)) {
break;
}
Expand All @@ -297,7 +287,6 @@ async function main() {
process.exit(1);
}
} finally {
// Cleanup archive
try {
fs.unlinkSync(archivePath);
} catch {
Expand Down
69 changes: 0 additions & 69 deletions services/deploy-infra/builder/src/cloudflare-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import * as z from 'zod';
import type { DeploymentFile, WorkerMetadata } from './types';
import type { PlaintextEnvVar } from '../../../../apps/web/src/lib/user-deployments/env-vars-validation';

/**
* Cloudflare API Client - Handles interactions with Cloudflare Workers API.
* Provides methods for asset uploads and worker deployments.
*/

/**
* Error thrown when a worker is not found in Cloudflare (error code 10007).
*/
Expand All @@ -17,9 +12,6 @@ export class WorkerNotFoundError extends Error {
}
}

/**
* Standard Cloudflare API response structure.
*/
const cloudflareErrorSchema = z.object({
code: z.number(),
message: z.string(),
Expand Down Expand Up @@ -53,31 +45,15 @@ export const parseCloudflareResponse = <T>(
};
};

/**
* CloudflareAPI class for interacting with Cloudflare Workers API.
* Handles asset upload sessions, batch uploads, and worker deployments.
*/
export class CloudflareAPI {
private accountId: string;
private apiToken: string;

/**
* Create a new CloudflareAPI instance.
*
* @param accountId - Cloudflare account ID
* @param apiToken - Cloudflare API token with Workers deployment permissions
*/
constructor(accountId: string, apiToken: string) {
this.accountId = accountId;
this.apiToken = apiToken;
}

/**
* Get headers for API requests.
*
* @param contentType - Optional Content-Type header value
* @returns Headers object with authorization and optional content type
*/
private getHeaders(contentType?: string): HeadersInit {
const headers: HeadersInit = {
Authorization: `Bearer ${this.apiToken}`,
Expand All @@ -90,9 +66,6 @@ export class CloudflareAPI {
return headers;
}

/**
* Retry a function with exponential backoff for transient errors
*/
private async retryWithBackoff<T>(
fn: () => Promise<T>,
operation: string,
Expand All @@ -106,14 +79,12 @@ export class CloudflareAPI {
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));

// Check if it's a 5xx error (transient)
const is5xxError = lastError.message.includes('status: 5');

if (!is5xxError || attempt === maxAttempts) {
throw lastError;
}

// Exponential backoff: 1s, 2s, 4s, etc., max 30s
const delayMs = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
console.log(
`${operation} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delayMs}ms...`
Expand All @@ -126,14 +97,6 @@ export class CloudflareAPI {
throw lastError ?? new Error('Unknown error in retry logic');
}

/**
* Create an asset upload session for a worker script.
*
* @param scriptName - Name of the worker script
* @param manifest - Manifest mapping file paths to their hash and size
* @param dispatchNamespace - Dispatch namespace for the worker
* @returns Upload session JWT and bucket assignments
*/
async createAssetUploadSession(
scriptName: string,
manifest: Record<string, { hash: string; size: number }>,
Expand Down Expand Up @@ -176,14 +139,6 @@ export class CloudflareAPI {
}, 'Create asset upload session');
}

/**
* Upload a batch of assets to Cloudflare.
*
* @param uploadToken - JWT token from the upload session
* @param fileHashes - Array of file hashes to upload in this batch
* @param fileContents - Map of hash to file content (Buffer) and MIME type
* @returns Completion JWT if all files uploaded (status 201), null otherwise
*/
async uploadAssetBatch(
scriptName: string,
uploadToken: string,
Expand All @@ -193,7 +148,6 @@ export class CloudflareAPI {
return await this.retryWithBackoff(async () => {
const url = `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/workers/assets/upload?base64=true`;

// Build FormData with each file
const formData = new FormData();

for (const hash of fileHashes) {
Expand All @@ -202,10 +156,8 @@ export class CloudflareAPI {
throw new Error(`Missing file content for hash ${hash}`);
}

// Convert Buffer to base64 string (matches VibeSDK pattern)
const base64Content = fileData.buffer.toString('base64');

// Create blob with base64 content as text
const blob = new Blob([base64Content], { type: fileData.mimeType });

// Append to form data with hash as both field name and filename
Expand Down Expand Up @@ -266,26 +218,17 @@ export class CloudflareAPI {
}, 'Upload asset batch');
}

/**
* Deploy a worker script to Cloudflare.
*/
async deployWorker(params: {
/** Name of the worker script */
scriptName: string;
/** Worker metadata including main module, compatibility settings, and assets */
metadata: WorkerMetadata;
/** Worker script file */
workerScript: DeploymentFile;
/** Dispatch namespace for the worker */
dispatchNamespace: string;
/** Optional array of artifact files to include in deployment */
artifacts?: DeploymentFile[];
/** Environment variables (only non-secret) */
envVars?: PlaintextEnvVar[];
}): Promise<void> {
const { scriptName, metadata, workerScript, dispatchNamespace, artifacts, envVars } = params;

// Assert that envVars don't contain any secret variables
if (envVars?.some(v => v.isSecret)) {
throw new Error(
'Secret environment variables must be set via setSecrets(), not deployWorker()'
Expand All @@ -295,7 +238,6 @@ export class CloudflareAPI {
return await this.retryWithBackoff(async () => {
const url = `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/workers/dispatch/namespaces/${dispatchNamespace}/scripts/${scriptName}`;

// Add plain text environment variables to metadata bindings
const plainTextBindings = (envVars || []).map(v => ({
type: 'plain_text',
name: v.key,
Expand All @@ -315,7 +257,6 @@ export class CloudflareAPI {
});
formData.append('index.js', workerBlob, 'index.js');

// Append artifact files if provided
if (artifacts && artifacts.length > 0) {
for (const artifact of artifacts) {
const artifactBlob = new Blob([new Uint8Array(artifact.content)], {
Expand Down Expand Up @@ -357,15 +298,13 @@ export class CloudflareAPI {
const existingClass = messageStr.match(/class "([^"]+)"/)?.[1];

if (existingClass && metadata.migrations) {
// Filter out the existing class from migrations
const filteredMigrations = metadata.migrations
.map(migration => ({
...migration,
new_classes: migration.new_classes?.filter(cls => cls !== existingClass),
}))
.filter(migration => !migration.new_classes || migration.new_classes.length > 0);

// Retry with filtered migrations (preserve envVars for retry)
return await this.deployWorker({
scriptName,
metadata: { ...metadata, migrations: filteredMigrations },
Expand All @@ -386,10 +325,6 @@ export class CloudflareAPI {
}, 'Deploy worker');
}

/**
* Sets secrets for a worker in a dispatch namespace using Cloudflare's Secrets API.
* Secrets are set individually via PUT requests, processed in parallel batches.
*/
async setSecrets(
scriptName: string,
dispatchNamespace: string,
Expand Down Expand Up @@ -437,11 +372,7 @@ export class CloudflareAPI {
}

/**
* Delete a worker script from a dispatch namespace.
* Note: Assets are automatically cleaned up when the script is deleted.
*
* @param scriptName - Name of the worker script to delete
* @param dispatchNamespace - Dispatch namespace containing the worker
*/
async deleteWorker(scriptName: string, dispatchNamespace: string): Promise<void> {
return await this.retryWithBackoff(async () => {
Expand Down
21 changes: 0 additions & 21 deletions services/deploy-infra/builder/src/deployer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* Deployer - Orchestrates the deployment of Cloudflare Workers with assets.
* Handles uploading assets and deploying worker scripts.
*/

import { WorkerNotFoundError, type CloudflareAPI } from './cloudflare-api';
import { DEPLOY_DISPATCH_NAMESPACE } from './dispatch-namespace';
import type { DeploymentArtifacts } from './types';
Expand Down Expand Up @@ -40,19 +35,11 @@ export class Deployer {
});
}

/**
* Deploy a worker with its assets to Cloudflare
*/
async deploy(params: {
/** Worker script and asset files */
artifacts: DeploymentArtifacts;
/** Name of the worker */
workerName: string;
/** Callback for logging progress */
logger: (message: string) => void;
/** Dispatch namespace to deploy to */
dispatchNamespace?: string;
/** Optional environment variables (decrypted) */
envVars?: PlaintextEnvVar[];
}): Promise<void> {
const {
Expand All @@ -71,7 +58,6 @@ export class Deployer {
const secretEnvVars = envVars?.filter(v => v.isSecret) ?? [];
const plainTextEnvVars = envVars?.filter(v => !v.isSecret) ?? [];

// Set secrets
if (secretEnvVars.length > 0) {
try {
await this.api.setSecrets(workerName, dispatchNamespace, secretEnvVars);
Expand All @@ -87,7 +73,6 @@ export class Deployer {
}
}

// Handle case with no assets and no artifacts
if (assets.length === 0 && artifactFiles.length === 0) {
logger('No assets or artifacts found, deploying worker only');
const metadata = {
Expand All @@ -112,7 +97,6 @@ export class Deployer {

logger(`Found ${assets.length} asset files and ${artifactFiles.length} artifact files`);

// Calculate hashes and build data structures
const fileContents = new Map<string, { buffer: Buffer; mimeType: string }>();
const manifest: Record<string, { hash: string; size: number }> = {};

Expand Down Expand Up @@ -140,7 +124,6 @@ export class Deployer {

logger(`Built asset manifest with ${assets.length} files, ${totalBytes} total bytes`);

// Create asset upload session
const { jwt, buckets } = await this.api.createAssetUploadSession(
workerName,
manifest,
Expand All @@ -149,15 +132,13 @@ export class Deployer {

logger(`Created asset upload session, ${buckets.length} buckets to upload`);

// Upload assets in batches
let completionJwt: string | null = null;

if (buckets.length === 0) {
// All assets already exist (deduplication), use session JWT as completion JWT
logger('All assets already exist on Cloudflare');
completionJwt = jwt;
} else {
// Upload new/changed assets
for (let i = 0; i < buckets.length; i++) {
const bucket = buckets[i];
logger(`Uploading batch ${i + 1}/${buckets.length} (${bucket.length} files)`);
Expand All @@ -176,7 +157,6 @@ export class Deployer {

logger('All assets uploaded successfully');

// Build worker metadata with asset configuration
const bindings: Array<Record<string, unknown>> = [
{
name: 'ASSETS',
Expand All @@ -195,7 +175,6 @@ export class Deployer {
},
};

// Deploy worker with artifact files
await this.api.deployWorker({
scriptName: workerName,
metadata,
Expand Down
Loading