Skip to content

Commit 44e20ca

Browse files
emilioaccclaude
andauthored
feat(paas): add --env and --env-file flags for worker deploy (#27)
Add support for setting environment variables when deploying workers: - --env KEY=VALUE flag (repeatable) for individual env vars - --env-file <path> flag to load from a .env file - --env takes precedence over --env-file for duplicate keys - Validation for reserved names (DB, BUCKET, ANALYTICS, USER_NAMESPACE) - Warning for sensitive-looking variables (SECRET, PASSWORD, KEY, etc.) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3e475e6 commit 44e20ca

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

packages/atxp/src/commands/paas/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ interface PaasOptions {
5858
event?: string;
5959
groupBy?: string;
6060
enableAnalytics?: boolean;
61+
env?: string[];
62+
envFile?: string;
6163
}
6264

6365
function showPaasHelp(): void {
@@ -67,6 +69,12 @@ function showPaasHelp(): void {
6769

6870
console.log(chalk.bold('Worker Commands:'));
6971
console.log(' ' + chalk.cyan('paas worker deploy') + ' ' + chalk.yellow('<name>') + ' Deploy a worker');
72+
console.log(' ' + chalk.gray('--code <file>') + ' Path to worker code file');
73+
console.log(' ' + chalk.gray('--db <binding:name>') + ' Bind a database (repeatable)');
74+
console.log(' ' + chalk.gray('--bucket <binding:name>') + ' Bind a storage bucket (repeatable)');
75+
console.log(' ' + chalk.gray('--env KEY=VALUE') + ' Set environment variable (repeatable)');
76+
console.log(' ' + chalk.gray('--env-file <path>') + ' Load env vars from file');
77+
console.log(' ' + chalk.gray('--enable-analytics') + ' Enable Analytics Engine binding');
7078
console.log(' ' + chalk.cyan('paas worker list') + ' List all workers');
7179
console.log(' ' + chalk.cyan('paas worker logs') + ' ' + chalk.yellow('<name>') + ' Get worker logs');
7280
console.log(' ' + chalk.cyan('paas worker delete') + ' ' + chalk.yellow('<name>') + ' Delete a worker');
@@ -171,6 +179,8 @@ async function handleWorkerCommand(
171179
db: options.db,
172180
bucket: options.bucket,
173181
enableAnalytics: options.enableAnalytics,
182+
env: options.env,
183+
envFile: options.envFile,
174184
});
175185
break;
176186

packages/atxp/src/commands/paas/worker.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,92 @@ interface WorkerDeployOptions {
1010
db?: string[];
1111
bucket?: string[];
1212
enableAnalytics?: boolean;
13+
env?: string[];
14+
envFile?: string;
15+
}
16+
17+
// Reserved env var names that conflict with existing bindings
18+
const RESERVED_ENV_NAMES = ['DB', 'BUCKET', 'ANALYTICS', 'USER_NAMESPACE'];
19+
20+
// Patterns that suggest sensitive data (warn user about plain text storage)
21+
const SENSITIVE_PATTERNS = [/SECRET/i, /PASSWORD/i, /KEY/i, /TOKEN/i, /CREDENTIAL/i];
22+
23+
/**
24+
* Validate an environment variable name
25+
* Must be a valid identifier and not reserved
26+
*/
27+
function validateEnvVarName(name: string): { valid: boolean; error?: string } {
28+
// Check if it's a valid identifier (starts with letter or underscore, contains only alphanumeric and underscores)
29+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
30+
return { valid: false, error: `Invalid env var name "${name}": must be a valid identifier (letters, numbers, underscores, cannot start with number)` };
31+
}
32+
33+
// Check if it's reserved
34+
if (RESERVED_ENV_NAMES.includes(name.toUpperCase())) {
35+
return { valid: false, error: `Reserved env var name "${name}": conflicts with existing bindings (${RESERVED_ENV_NAMES.join(', ')})` };
36+
}
37+
38+
return { valid: true };
39+
}
40+
41+
/**
42+
* Parse a KEY=VALUE string into key and value
43+
*/
44+
function parseEnvArg(arg: string): { key: string; value: string } | null {
45+
const eqIndex = arg.indexOf('=');
46+
if (eqIndex === -1) {
47+
return null;
48+
}
49+
const key = arg.slice(0, eqIndex);
50+
const value = arg.slice(eqIndex + 1);
51+
return { key, value };
52+
}
53+
54+
/**
55+
* Parse a .env file into a key-value record
56+
* Supports:
57+
* - KEY=VALUE format
58+
* - Comments starting with #
59+
* - Empty lines
60+
* - Quoted values (single or double quotes)
61+
*/
62+
function parseEnvFile(filePath: string): Record<string, string> {
63+
const absolutePath = path.resolve(filePath);
64+
if (!fs.existsSync(absolutePath)) {
65+
throw new Error(`Env file not found: ${absolutePath}`);
66+
}
67+
68+
const content = fs.readFileSync(absolutePath, 'utf-8');
69+
const result: Record<string, string> = {};
70+
71+
for (const line of content.split('\n')) {
72+
const trimmed = line.trim();
73+
74+
// Skip empty lines and comments
75+
if (!trimmed || trimmed.startsWith('#')) {
76+
continue;
77+
}
78+
79+
const eqIndex = trimmed.indexOf('=');
80+
if (eqIndex === -1) {
81+
continue;
82+
}
83+
84+
const key = trimmed.slice(0, eqIndex).trim();
85+
let value = trimmed.slice(eqIndex + 1).trim();
86+
87+
// Remove surrounding quotes if present
88+
if ((value.startsWith('"') && value.endsWith('"')) ||
89+
(value.startsWith("'") && value.endsWith("'"))) {
90+
value = value.slice(1, -1);
91+
}
92+
93+
if (key) {
94+
result[key] = value;
95+
}
96+
}
97+
98+
return result;
1399
}
14100

15101
interface WorkerLogsOptions {
@@ -55,6 +141,51 @@ export async function workerDeployCommand(
55141
};
56142
});
57143

144+
// Process environment variables
145+
// Start with env file (lower precedence)
146+
const envVars: Record<string, string> = {};
147+
148+
if (options.envFile) {
149+
try {
150+
const fileEnvVars = parseEnvFile(options.envFile);
151+
Object.assign(envVars, fileEnvVars);
152+
} catch (error) {
153+
console.error(chalk.red(`Error: ${(error as Error).message}`));
154+
process.exit(1);
155+
}
156+
}
157+
158+
// Process --env flags (higher precedence, overrides file)
159+
if (options.env && options.env.length > 0) {
160+
for (const envArg of options.env) {
161+
const parsed = parseEnvArg(envArg);
162+
if (!parsed) {
163+
console.error(chalk.red(`Error: Invalid env var format "${envArg}". Expected KEY=VALUE`));
164+
process.exit(1);
165+
}
166+
envVars[parsed.key] = parsed.value;
167+
}
168+
}
169+
170+
// Validate all env var names
171+
for (const key of Object.keys(envVars)) {
172+
const validation = validateEnvVarName(key);
173+
if (!validation.valid) {
174+
console.error(chalk.red(`Error: ${validation.error}`));
175+
process.exit(1);
176+
}
177+
}
178+
179+
// Warn about sensitive-looking variables
180+
const sensitiveVars = Object.keys(envVars).filter((key) =>
181+
SENSITIVE_PATTERNS.some((pattern) => pattern.test(key))
182+
);
183+
if (sensitiveVars.length > 0) {
184+
console.log(chalk.yellow(`Warning: The following env vars may contain sensitive data and will be stored as plain text:`));
185+
console.log(chalk.yellow(` ${sensitiveVars.join(', ')}`));
186+
console.log(chalk.yellow(` Consider using Cloudflare Secrets for sensitive values.`));
187+
}
188+
58189
const args: Record<string, unknown> = { name, code };
59190
if (databaseBindings && databaseBindings.length > 0) {
60191
args.database_bindings = databaseBindings;
@@ -65,6 +196,9 @@ export async function workerDeployCommand(
65196
if (options.enableAnalytics) {
66197
args.enable_analytics = true;
67198
}
199+
if (Object.keys(envVars).length > 0) {
200+
args.env_vars = envVars;
201+
}
68202

69203
const result = await callTool(SERVER, 'deploy_worker', args);
70204
console.log(result);

packages/atxp/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ interface PaasOptions {
5757
event?: string;
5858
groupBy?: string;
5959
enableAnalytics?: boolean;
60+
env?: string[];
61+
envFile?: string;
6062
}
6163

6264
// Parse command line arguments
@@ -176,6 +178,8 @@ function parseArgs(): {
176178
event: getArgValue('--event', ''),
177179
groupBy: getArgValue('--group-by', ''),
178180
enableAnalytics: process.argv.includes('--enable-analytics'),
181+
env: getAllArgValues('--env'),
182+
envFile: getArgValue('--env-file', ''),
179183
};
180184

181185
// Get PAAS args (everything after 'paas' that doesn't start with -)

0 commit comments

Comments
 (0)