fix(release): execute d1 migrations without shell

This commit is contained in:
ZacharyZhang-NY committed 2026-04-22 14:52:54 -04:00
1 parent cb315f308d
commit eec26a09a3
4 files changed
+270 -31

No files matched your search

+2 -19
View File
@@ -7,6 +7,7 @@ import { loadEnvFile } from "./release/lib/dotenv.mjs";
import { createRunner } from "./release/lib/process.mjs";
import { createWorkflowHelpers } from "./release/lib/workflow.mjs";
import { createSnapshotHelpers } from "./release/lib/snapshot.mjs";
import { applyD1Migrations } from "./release/lib/d1-migrations.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
@@ -221,25 +222,7 @@ async function uploadWinAndLinuxToR2() {
}
async function applyLandingMigrations() {
const migrationsDir = join(landingDir, "migrations");
if (!existsSync(migrationsDir)) {
return;
}
const wranglerToml = await readFile(join(landingDir, "wrangler.toml"), "utf8");
const match = wranglerToml.match(/database_name\s*=\s*"([^"]+)"/);
if (!match) {
return;
}
const databaseName = match[1];
const migrationFiles = (await readdir(migrationsDir))
.filter((fileName) => fileName.endsWith(".sql"))
.sort();
for (const fileName of migrationFiles) {
const command = `printf 'y\\n' | npx wrangler d1 execute ${databaseName} --remote --file migrations/${fileName}`;
await run("bash", ["-lc", command], { cwd: landingDir });
}
await applyD1Migrations({ landingDir, run });
}
async function buildAndDeployLanding() {
+129
View File
@@ -0,0 +1,129 @@
import { existsSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
const databaseNamePattern = /^[A-Za-z0-9_-]+$/;
const migrationFilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*\.sql$/;
function stripTomlComment(line) {
let inString = false;
let escaped = false;
for (let i = 0; i < line.length; i += 1) {
const char = line[i];
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
if (char === "\"") {
inString = !inString;
continue;
}
if (char === "#" && !inString) {
return line.slice(0, i);
}
}
return line;
}
function parseTomlString(value, key) {
const trimmed = value.trim();
if (!trimmed.startsWith("\"") || !trimmed.endsWith("\"")) {
throw new Error(`wrangler.toml key "${key}" must be a quoted string.`);
}
try {
return JSON.parse(trimmed);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`wrangler.toml key "${key}" has invalid string syntax: ${detail}`);
}
}
function parseAssignment(line) {
const equals = line.indexOf("=");
if (equals === -1) return null;
const key = line.slice(0, equals).trim();
const value = line.slice(equals + 1).trim();
if (!key) return null;
return { key, value };
}
export function validateD1DatabaseName(databaseName) {
if (!databaseNamePattern.test(databaseName)) {
throw new Error(
`Invalid Cloudflare D1 database_name "${databaseName}". Use only letters, numbers, underscores, and hyphens.`,
);
}
return databaseName;
}
export function validateD1MigrationFileName(fileName) {
if (!migrationFilePattern.test(fileName)) {
throw new Error(
`Invalid D1 migration file name "${fileName}". Use a plain .sql file name with letters, numbers, dots, underscores, and hyphens.`,
);
}
return fileName;
}
export function parseD1DatabaseName(wranglerToml) {
let inD1Database = false;
for (const rawLine of wranglerToml.split(/\r?\n/)) {
const line = stripTomlComment(rawLine).trim();
if (!line) continue;
if (line.startsWith("[[")) {
inD1Database = line === "[[d1_databases]]";
continue;
}
if (line.startsWith("[")) {
inD1Database = false;
continue;
}
if (!inD1Database) continue;
const assignment = parseAssignment(line);
if (!assignment || assignment.key !== "database_name") continue;
return validateD1DatabaseName(
parseTomlString(assignment.value, assignment.key),
);
}
throw new Error("landing/wrangler.toml does not define [[d1_databases]].database_name.");
}
export async function listD1MigrationFiles(migrationsDir) {
return (await readdir(migrationsDir))
.filter((fileName) => fileName.endsWith(".sql"))
.map(validateD1MigrationFileName)
.sort();
}
export async function applyD1Migrations({ landingDir, run }) {
const migrationsDir = join(landingDir, "migrations");
if (!existsSync(migrationsDir)) {
return { databaseName: null, applied: 0 };
}
const wranglerToml = await readFile(join(landingDir, "wrangler.toml"), "utf8");
const databaseName = parseD1DatabaseName(wranglerToml);
const migrationFiles = await listD1MigrationFiles(migrationsDir);
for (const fileName of migrationFiles) {
await run(
"npx",
[
"wrangler",
"d1",
"execute",
databaseName,
"--remote",
"--file",
`migrations/${fileName}`,
],
{ cwd: landingDir, input: "y\n" },
);
}
return { databaseName, applied: migrationFiles.length };
}
+3 -12
View File
@@ -1,11 +1,12 @@
import { existsSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { prepareR2ReleaseHost, uploadReleaseFiles } from "./release-local/r2.mjs";
import { loadEnvFile } from "./release/lib/dotenv.mjs";
import { createRunner } from "./release/lib/process.mjs";
import { createWorkflowHelpers } from "./release/lib/workflow.mjs";
import { applyD1Migrations } from "./release/lib/d1-migrations.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
@@ -37,17 +38,7 @@ async function uploadWinAndLinuxToR2() {
}
async function applyLandingMigrations() {
const migrationsDir = join(landingDir, "migrations");
if (!existsSync(migrationsDir)) return;
const wranglerToml = await readFile(join(landingDir, "wrangler.toml"), "utf8");
const match = wranglerToml.match(/database_name\s*=\s*"([^"]+)"/);
if (!match) return;
const databaseName = match[1];
const migrationFiles = (await readdir(migrationsDir)).filter((n) => n.endsWith(".sql")).sort();
for (const fileName of migrationFiles) {
const command = `printf 'y\\n' | npx wrangler d1 execute ${databaseName} --remote --file migrations/${fileName}`;
await run("bash", ["-lc", command], { cwd: landingDir });
}
await applyD1Migrations({ landingDir, run });
}
async function buildAndDeployLanding() {