-
Notifications
You must be signed in to change notification settings - Fork 80
Validate Workers deploy config #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,21 +13,82 @@ interface Config { | |
| vars?: Record<string, string>; | ||
| } | ||
|
|
||
| function requireText(value: string | undefined, field: string): string { | ||
| const text = value?.trim(); | ||
| if (!text) throw new Error(`deploy-workers requires ${field}`); | ||
| return text; | ||
| } | ||
|
|
||
| function optionalText(value: string | undefined, field: string): string | undefined { | ||
| return value === undefined ? undefined : requireText(value, field); | ||
| } | ||
|
|
||
| function requireSegment(value: string | undefined, field: string): string { | ||
| const text = requireText(value, field); | ||
| if (/[\\/?#\s\x00-\x1F\x7F]/.test(text)) { | ||
| throw new Error(`deploy-workers ${field} must be a single URL-safe segment`); | ||
| } | ||
| return text; | ||
| } | ||
|
|
||
| function compatibilityDate(value: string | undefined): string | undefined { | ||
| const date = optionalText(value, 'compatibilityDate'); | ||
| if (date && !/^\d{4}-\d{2}-\d{2}$/.test(date)) { | ||
| throw new Error('deploy-workers compatibilityDate must use YYYY-MM-DD'); | ||
| } | ||
| return date; | ||
| } | ||
|
|
||
| function routes(value: string[] | undefined): string[] | undefined { | ||
| return value?.map((route, index) => requireText(route, `routes[${index}]`)); | ||
| } | ||
|
|
||
| function workerVars(value: Record<string, string> | undefined): Record<string, string> | undefined { | ||
| if (value === undefined) return undefined; | ||
| for (const [key, entryValue] of Object.entries(value)) { | ||
| if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { | ||
| throw new Error(`deploy-workers var "${key}" must be a valid environment variable name`); | ||
| } | ||
| if (typeof entryValue !== 'string') { | ||
| throw new Error(`deploy-workers var "${key}" must be a string`); | ||
| } | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function normalizedConfig(config: Config): Config { | ||
| return { | ||
| ...config, | ||
| name: requireSegment(config.name, 'name'), | ||
| accountId: requireSegment(config.accountId, 'accountId'), | ||
| routes: routes(config.routes), | ||
| env: optionalText(config.env, 'env'), | ||
| compatibilityDate: compatibilityDate(config.compatibilityDate), | ||
| entrypoint: optionalText(config.entrypoint, 'entrypoint'), | ||
| configPath: optionalText(config.configPath, 'configPath'), | ||
| vars: workerVars(config.vars), | ||
| }; | ||
| } | ||
|
|
||
| function workerEntry(ctx: { projectDir: string }, config: Config): string | undefined { | ||
| config = normalizedConfig(config); | ||
| if (!config.entrypoint) return undefined; | ||
| return isAbsolute(config.entrypoint) ? config.entrypoint : join(ctx.projectDir, config.entrypoint); | ||
| } | ||
|
|
||
| function wranglerConfig(ctx: { projectDir: string }, config: Config): string | undefined { | ||
| config = normalizedConfig(config); | ||
| if (!config.configPath) return undefined; | ||
| return isAbsolute(config.configPath) ? config.configPath : join(ctx.projectDir, config.configPath); | ||
| } | ||
|
|
||
| function deployEnv(ctx: { channel: string }, config: Config): string { | ||
| config = normalizedConfig(config); | ||
| return config.env ?? (ctx.channel === 'stable' ? 'production' : 'preview'); | ||
| } | ||
|
|
||
| function deployArgs(ctx: { channel: string; projectDir: string }, config: Config, opts: { dryRun?: boolean } = {}): string[] { | ||
| config = normalizedConfig(config); | ||
| const args = ['--yes', 'wrangler', 'deploy']; | ||
| const entrypoint = workerEntry(ctx, config); | ||
| if (entrypoint) args.push(entrypoint); | ||
|
|
@@ -43,6 +104,7 @@ function deployArgs(ctx: { channel: string; projectDir: string }, config: Config | |
| } | ||
|
|
||
| function renderPlan(ctx: { channel: string; projectDir: string; version: string }, config: Config): string { | ||
| config = normalizedConfig(config); | ||
|
Comment on lines
73
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| return `${JSON.stringify({ | ||
| provider: 'cloudflare-workers', | ||
| name: config.name, | ||
|
|
@@ -67,13 +129,15 @@ export default defineTarget<Config>({ | |
| kind: 'web', | ||
| label: 'Cloudflare Workers', | ||
| async build(ctx, config) { | ||
| config = normalizedConfig(config); | ||
| const planPath = join(ctx.outDir, 'workers-deploy.json'); | ||
| ctx.log(`wrangler deploy --dry-run - name=${config.name}`); | ||
| await mkdir(ctx.outDir, { recursive: true }); | ||
| await writeFile(planPath, renderPlan(ctx, config), 'utf-8'); | ||
| return { artifact: planPath }; | ||
| }, | ||
| async ship(ctx, config) { | ||
| config = normalizedConfig(config); | ||
| const env = deployEnv(ctx, config); | ||
| ctx.log(`wrangler deploy - name=${config.name} - env=${env}`); | ||
| if (ctx.dryRun) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^\d{4}-\d{2}-\d{2}$only validates the format, not calendar validity. A value like'2026-13-45'satisfies the regex but is not a real date, and Wrangler would reject it at deploy time. AddingisNaN(Date.parse(date))as a secondary guard would catch logically invalid dates early.