diff --git a/docs/agent-deploy-cloudflare.md b/docs/agent-deploy-cloudflare.md index deb1d9a2..4c262038 100644 --- a/docs/agent-deploy-cloudflare.md +++ b/docs/agent-deploy-cloudflare.md @@ -36,7 +36,7 @@ This document defines the standard operating specifications and conventions for Deploy command: bun run deploy:cloudflare-builds ``` - - The deploy command automatically resolves the D1 UUID from the `edgeever` database name and writes it only to a temporary generated Wrangler configuration. Users do not need to edit `wrangler.toml` or copy a D1 ID into a build variable. + - The deploy command automatically resolves the D1 UUID from the `edgeever` database name and writes it only to a temporary generated Wrangler configuration. The tracked `wrangler.toml` must keep its all-zero `database_id` placeholder; deployment rejects an instance-specific ID committed there so a Fork cannot diverge on local infrastructure configuration. - Ensure the Workers Builds API token can read and edit D1. An advanced deployment that uses a differently named database must set the build variable `EDGE_EVER_D1_DATABASE_ID` explicitly. 6. **Start Initial Build & Verify Service** diff --git a/docs/agent-deploy-cloudflare.zh-CN.md b/docs/agent-deploy-cloudflare.zh-CN.md index 534d9c31..0e489adf 100644 --- a/docs/agent-deploy-cloudflare.zh-CN.md +++ b/docs/agent-deploy-cloudflare.zh-CN.md @@ -36,7 +36,7 @@ Deploy command: bun run deploy:cloudflare-builds ``` - - 部署命令会根据 `edgeever` 数据库名称自动查询 D1 UUID,并且只写入临时生成的 Wrangler 配置。用户无需修改 `wrangler.toml`,也无需手工把 D1 ID 复制到构建变量。 + - 部署命令会根据 `edgeever` 数据库名称自动查询 D1 UUID,并且只写入临时生成的 Wrangler 配置。受版本控制的 `wrangler.toml` 必须保留全零 `database_id` 占位符;若把实例专属 ID 提交到该文件,部署会直接拒绝,避免 Fork 因本地基础设施配置产生分叉。 - 请确保 Workers Builds API Token 具有 D1 读取和编辑权限。使用其他数据库名称的高级部署需要显式设置构建变量 `EDGE_EVER_D1_DATABASE_ID`。 6. **启动首次构建与服务验证** diff --git a/docs/deploy-cloudflare-button.md b/docs/deploy-cloudflare-button.md index afb77e1e..4ce1dab2 100644 --- a/docs/deploy-cloudflare-button.md +++ b/docs/deploy-cloudflare-button.md @@ -69,7 +69,7 @@ Deploy command: bun run deploy:cloudflare-builds Click **Save and Deploy** to trigger the initial build. -The deploy command automatically looks up the D1 UUID by the `edgeever` database name. Do not edit `wrangler.toml` or manually copy the D1 ID. The Workers Builds API token must have D1 read/edit permission. +The deploy command automatically looks up the D1 UUID by the `edgeever` database name. Keep the tracked `wrangler.toml` on its all-zero `database_id` placeholder; deployment rejects an instance-specific ID committed there. The Workers Builds API token must have D1 read/edit permission. After publishing, the CI deployment records the actual public target reported by Wrangler and requests its `/api/health` endpoint. The build fails if the live Worker is missing its `DB` binding, uses an unprepared D1 database, or does not return a healthy response. diff --git a/docs/deploy-cloudflare-button.zh-CN.md b/docs/deploy-cloudflare-button.zh-CN.md index 33cd8ba9..e0d5ead3 100644 --- a/docs/deploy-cloudflare-button.zh-CN.md +++ b/docs/deploy-cloudflare-button.zh-CN.md @@ -69,7 +69,7 @@ Deploy command: bun run deploy:cloudflare-builds 点击 **Save and Deploy** 启动首次构建部署。 -部署命令会根据 `edgeever` 数据库名称自动查询 D1 UUID。无需修改 `wrangler.toml`,也无需手工复制 D1 ID。Workers Builds API Token 必须具有 D1 读取和编辑权限。 +部署命令会根据 `edgeever` 数据库名称自动查询 D1 UUID。受版本控制的 `wrangler.toml` 必须保留全零 `database_id` 占位符;若把实例专属 ID 提交到该文件,部署会直接拒绝。Workers Builds API Token 必须具有 D1 读取和编辑权限。 发布完成后,CI 部署会记录 Wrangler 返回的实际公网入口,并请求该入口的 `/api/health`。如果线上 Worker 缺少 `DB` binding、绑定了未初始化的 D1,或没有返回健康状态,构建会直接失败。 diff --git a/scripts/run-wrangler.mjs b/scripts/run-wrangler.mjs index f73b5a60..186c68a2 100644 --- a/scripts/run-wrangler.mjs +++ b/scripts/run-wrangler.mjs @@ -4,6 +4,7 @@ import { readFileSync, readdirSync, rmSync, + writeSync, writeFileSync, } from "node:fs"; import { dirname, resolve } from "node:path"; @@ -13,12 +14,13 @@ import { findD1DatabaseIdByName, normalizeD1MigrationSql, parseWranglerDeploymentUrls, + PLACEHOLDER_D1_ID, + repositoryD1ConfigError, runWranglerSync, shouldCaptureDeploymentTargets, } from "./wrangler-runner.mjs"; import { writeWranglerNotice } from "./wrangler-output.mjs"; -const PLACEHOLDER_D1_ID = "00000000-0000-0000-0000-000000000000"; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -72,7 +74,9 @@ if (requestedInstance !== undefined) { process.env.EDGE_EVER_INSTANCE = requestedInstance; } -const baseConfigPath = resolve(process.env.WRANGLER_CONFIG ?? "wrangler.toml"); +const repositoryConfigPath = resolve("wrangler.toml"); +const baseConfigPath = resolve(process.env.WRANGLER_CONFIG ?? repositoryConfigPath); +const usesRepositoryConfig = baseConfigPath === repositoryConfigPath; const baseConfigDirectory = dirname(baseConfigPath); const instance = process.env.EDGE_EVER_INSTANCE?.trim(); const instanceKey = instance?.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase(); @@ -90,6 +94,12 @@ const generatedLocalDevEnvPath = resolve(".env.wrangler.generated.local"); let config = readFileSync(baseConfigPath, "utf8"); let changed = false; +const d1ConfigError = repositoryD1ConfigError(config, usesRepositoryConfig); +if (d1ConfigError) { + writeSync(2, `${d1ConfigError}\n`); + process.exit(1); +} + const migrationCommand = wranglerArgs[0] === "d1" && wranglerArgs[1] === "migrations" @@ -306,7 +316,7 @@ if (isRemoteCommand && config.includes(`database_id = "${PLACEHOLDER_D1_ID}"`)) instanceKey ? `Set EDGE_EVER_${instanceKey}_D1_DATABASE_ID or EDGE_EVER_D1_DATABASE_ID,` : "Set EDGE_EVER_D1_DATABASE_ID,", - "or replace the database_id placeholder in wrangler.toml / WRANGLER_CONFIG.", + "or point WRANGLER_CONFIG at an external configuration file with the required binding.", ].join(" "), ); process.exit(1); diff --git a/scripts/wrangler-runner.mjs b/scripts/wrangler-runner.mjs index 6d3b4b06..c5d747f1 100644 --- a/scripts/wrangler-runner.mjs +++ b/scripts/wrangler-runner.mjs @@ -2,6 +2,22 @@ import { spawnSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; +export const PLACEHOLDER_D1_ID = "00000000-0000-0000-0000-000000000000"; + +export const repositoryD1ConfigError = (config, usesRepositoryConfig) => { + if (!usesRepositoryConfig) return undefined; + + const configuredId = config.match(/^database_id\s*=\s*"([^"]+)"/m)?.[1]; + if (configuredId === PLACEHOLDER_D1_ID) return undefined; + + return [ + "Refusing to use an instance-specific D1 database id from the repository wrangler.toml.", + `Restore database_id to "${PLACEHOLDER_D1_ID}".`, + "Provide the real id through EDGE_EVER_D1_DATABASE_ID, let deployment resolve it by database name,", + "or point WRANGLER_CONFIG at an external configuration file.", + ].join(" "); +}; + export const resolveWranglerCliPath = (cwd = resolve(".")) => resolve(cwd, "node_modules", "wrangler", "bin", "wrangler.js"); diff --git a/tests/deployment-entrypoints.test.ts b/tests/deployment-entrypoints.test.ts index b3aeeae8..e0e0af34 100644 --- a/tests/deployment-entrypoints.test.ts +++ b/tests/deployment-entrypoints.test.ts @@ -15,6 +15,7 @@ import { manualDeploymentCopy, } from "../apps/site/src/deployment-prompts"; import { decideUpstreamSync } from "../scripts/upstream-sync-plan.mjs"; +import { repositoryD1ConfigError } from "../scripts/wrangler-runner.mjs"; const repositoryRoot = resolve(import.meta.dir, ".."); const normalizeLineEndings = (value: string) => value.replace(/\r\n/g, "\n"); @@ -123,15 +124,43 @@ describe("Cloudflare deployment entrypoints", () => { test("online deployment resolves the D1 id without editing the repository config", () => { const runner = readRepositoryFile("scripts/run-wrangler.mjs"); + const wranglerConfig = readRepositoryFile("wrangler.toml"); const englishAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.md"); const chineseAgentDoc = readRepositoryFile("docs/agent-deploy-cloudflare.zh-CN.md"); + expect(wranglerConfig).toContain( + 'database_id = "00000000-0000-0000-0000-000000000000"', + ); expect(runner).toContain('"d1", "list", "--json"'); expect(runner).toContain("findD1DatabaseIdByName"); + expect(runner).toContain("repositoryD1ConfigError(config, usesRepositoryConfig)"); + expect(runner).not.toContain("replace the database_id placeholder"); expect(englishAgentDoc).toContain("automatically resolves the D1 UUID"); expect(chineseAgentDoc).toContain("自动查询 D1 UUID"); }); + test("rejects an instance-specific D1 id in the repository Wrangler config", () => { + const instanceConfig = [ + 'name = "edgeever"', + 'database_name = "edgeever"', + 'database_id = "11111111-1111-1111-1111-111111111111"', + "", + ].join("\n"); + const placeholderConfig = instanceConfig.replace( + "11111111-1111-1111-1111-111111111111", + "00000000-0000-0000-0000-000000000000", + ); + + const error = repositoryD1ConfigError(instanceConfig, true); + expect(error).toContain( + "Refusing to use an instance-specific D1 database id from the repository wrangler.toml", + ); + expect(error).toContain("EDGE_EVER_D1_DATABASE_ID"); + expect(error).toContain("WRANGLER_CONFIG"); + expect(repositoryD1ConfigError(placeholderConfig, true)).toBeUndefined(); + expect(repositoryD1ConfigError(instanceConfig, false)).toBeUndefined(); + }); + test("keeps D1 resolver diagnostics out of Wrangler JSON stdout", () => { const workingDirectory = mkdtempSync(resolve(tmpdir(), "edgeever-wrangler-output-")); const wranglerBinDirectory = resolve( @@ -149,13 +178,13 @@ describe("Cloudflare deployment entrypoints", () => { const environment = { ...inheritedEnvironment, EDGE_EVER_INSTANCE: "", - WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.toml"), + WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.external.toml"), }; try { mkdirSync(wranglerBinDirectory, { recursive: true }); writeFileSync( - resolve(workingDirectory, "wrangler.toml"), + resolve(workingDirectory, "wrangler.external.toml"), [ 'name = "edgeever"', 'database_name = "edgeever"', @@ -255,7 +284,7 @@ describe("Cloudflare deployment entrypoints", () => { try { mkdirSync(wranglerBinDirectory, { recursive: true }); writeFileSync( - resolve(workingDirectory, "wrangler.toml"), + resolve(workingDirectory, "wrangler.external.toml"), [ 'name = "edgeever"', "workers_dev = true", @@ -288,7 +317,7 @@ describe("Cloudflare deployment entrypoints", () => { CI: "true", EDGE_EVER_AUTH_PASSWORD: "test-password", EDGE_EVER_INSTANCE: "", - WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.toml"), + WRANGLER_CONFIG: resolve(workingDirectory, "wrangler.external.toml"), }, }, );