From 768d3bd8d74ebe9aa0991b4f4900538d9b3389c0 Mon Sep 17 00:00:00 2001 From: Okiki Ojo Date: Wed, 1 Apr 2026 14:04:17 -0400 Subject: [PATCH] feat: enhance deployment process with environment variable support for D1 database configuration Signed-off-by: Okiki Ojo --- README.md | 10 +++++ package.json | 3 +- scripts/common.ts | 10 ++++- scripts/prepare-deploy.ts | 88 +++++++++++++++++++++++++++++++++++++++ scripts/verify-setup.ts | 4 +- 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 scripts/prepare-deploy.ts diff --git a/README.md b/README.md index bdf01ca..f832487 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,14 @@ npx wrangler d1 create cloudflare-ddns-db Copy the `database_id` from the output and paste it into `wrangler.jsonc` replacing the placeholder `00000000-0000-0000-0000-000000000000`, or use `pnpm setup:db` to do that for you. +If you deploy from CI or another automated pipeline, keep the placeholder in git and inject the real value at deploy time instead: + +```sh +D1_DATABASE_ID= pnpm deploy +``` + +If your remote database is not named `cloudflare-ddns-db`, also set `D1_DATABASE_NAME`. The `predeploy` script writes those values into `wrangler.jsonc` in the deploy workspace before validation and migrations run. + ### 3. Set secrets ```sh @@ -144,6 +152,8 @@ pnpm deploy This runs D1 migrations automatically before deploying. +For automated deploys, export `D1_DATABASE_ID` in the job environment before running `pnpm deploy`. You only need `D1_DATABASE_NAME` if the bound database name differs from the default. + ## Environment variables These non-secret variables live in `wrangler.jsonc` and can be overridden per-environment: diff --git a/package.json b/package.json index 829ad48..0e0049b 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,11 @@ "setup": "node --experimental-strip-types ./scripts/setup.ts", "setup:db": "node --experimental-strip-types ./scripts/setup-db.ts", "setup:secrets": "node --experimental-strip-types ./scripts/setup-secrets.ts", + "prepare-deploy": "node --experimental-strip-types ./scripts/prepare-deploy.ts", "verify-setup": "node --experimental-strip-types ./scripts/verify-setup.ts", "typecheck:scripts": "tsc -p tsconfig.scripts.json", "deploy": "wrangler deploy", - "predeploy": "node --experimental-strip-types ./scripts/verify-setup.ts && wrangler d1 migrations apply DB --remote", + "predeploy": "node --experimental-strip-types ./scripts/prepare-deploy.ts && node --experimental-strip-types ./scripts/verify-setup.ts && wrangler d1 migrations apply DB --remote", "dev": "wrangler d1 migrations apply DB --local && wrangler dev", "test": "wrangler deploy --dry-run && npx vitest run --config tests/vitest.config.mts" }, diff --git a/scripts/common.ts b/scripts/common.ts index 0649c24..4fdc6f5 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -1,5 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; +import process from "node:process"; + import { spawnSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { createInterface } from "node:readline/promises"; @@ -25,6 +27,8 @@ export const REQUIRED_SECRETS = [ export const REQUIRED_VARS = ["DDNS_ALLOWED_HOSTNAMES"] as const; export const PLACEHOLDER_DATABASE_ID = "00000000-0000-0000-0000-000000000000"; +export const D1_DATABASE_ID_ENV = "D1_DATABASE_ID"; +export const D1_DATABASE_NAME_ENV = "D1_DATABASE_NAME"; const uuidSchema = z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); const zoneIdSchema = z.string().regex(/^[0-9a-f]{32}$/i, "CF_ZONE_ID should be a 32-character hexadecimal zone ID."); @@ -358,4 +362,8 @@ export function parseSecretList(output: string): RequiredSecret[] { export function getRequiredVar(config: WranglerConfig, name: RequiredVar): string { return config.vars?.[name]?.trim() ?? ""; -} \ No newline at end of file +} + +export function getOptionalEnvVar(name: string): string { + return process.env[name]?.trim() ?? ""; +} diff --git a/scripts/prepare-deploy.ts b/scripts/prepare-deploy.ts new file mode 100644 index 0000000..5164c2b --- /dev/null +++ b/scripts/prepare-deploy.ts @@ -0,0 +1,88 @@ +import { + D1_DATABASE_ID_ENV, + D1_DATABASE_NAME_ENV, + getOptionalEnvVar, + getPrimaryD1Binding, + info, + isMainModule, + isPlaceholderDatabaseId, + isUuid, + readWranglerConfig, + writeWranglerConfig, +} from "./common.ts"; +import process from "node:process"; + +function envSummary(databaseId: string, databaseName: string): string { + if (databaseId && databaseName) { + return `${D1_DATABASE_ID_ENV} and ${D1_DATABASE_NAME_ENV}`; + } + + if (databaseId) { + return D1_DATABASE_ID_ENV; + } + + return D1_DATABASE_NAME_ENV; +} + +export async function prepareDeploy(): Promise { + const config = await readWranglerConfig(); + const existingBinding = getPrimaryD1Binding(config); + const envDatabaseId = getOptionalEnvVar(D1_DATABASE_ID_ENV); + const envDatabaseName = getOptionalEnvVar(D1_DATABASE_NAME_ENV); + + if (!envDatabaseId && !envDatabaseName) { + return false; + } + + if (envDatabaseId && (!isUuid(envDatabaseId) || isPlaceholderDatabaseId(envDatabaseId))) { + throw new Error(`${D1_DATABASE_ID_ENV} must be a real D1 database UUID.`); + } + + const nextBinding = { + binding: existingBinding?.binding || "DB", + database_name: envDatabaseName || existingBinding?.database_name || `${config.name}-db`, + database_id: envDatabaseId || existingBinding?.database_id || "", + }; + + if (!nextBinding.database_id) { + throw new Error( + `No D1 database_id is configured. Set ${D1_DATABASE_ID_ENV} for this deploy or run \`pnpm setup:db\` locally.`, + ); + } + + if (!isUuid(nextBinding.database_id) || isPlaceholderDatabaseId(nextBinding.database_id)) { + throw new Error( + `The configured D1 database_id is still a placeholder. Set ${D1_DATABASE_ID_ENV} for this deploy or run \`pnpm setup:db\` locally.`, + ); + } + + const bindingUnchanged = + existingBinding?.binding === nextBinding.binding && + existingBinding?.database_name === nextBinding.database_name && + existingBinding?.database_id === nextBinding.database_id; + + if (bindingUnchanged) { + return false; + } + + config.d1_databases = [ + nextBinding, + ...(config.d1_databases?.slice(1) ?? []), + ]; + + await writeWranglerConfig(config); + await info(`Updated wrangler.jsonc D1 binding from ${envSummary(envDatabaseId, envDatabaseName)}.`); + return true; +} + +async function main(): Promise { + await prepareDeploy(); +} + +if (isMainModule(import.meta.url)) { + main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(message); + process.exitCode = 1; + }); +} diff --git a/scripts/verify-setup.ts b/scripts/verify-setup.ts index bd1855f..f6670ce 100644 --- a/scripts/verify-setup.ts +++ b/scripts/verify-setup.ts @@ -25,7 +25,9 @@ export async function verifySetup(): Promise { if (!binding) { errors.push("Missing D1 binding `DB` in wrangler.jsonc."); } else if (!binding.database_id || !isUuid(binding.database_id) || isPlaceholderDatabaseId(binding.database_id)) { - errors.push("D1 database_id is missing or still set to the placeholder. Run `pnpm setup:db`."); + errors.push( + "D1 database_id is missing or still set to the placeholder. Run `pnpm setup:db`, or set D1_DATABASE_ID before automated deploys.", + ); } const missingSecretsConfig = missingRequiredSecrets(config); -- 2.51.2